diff --git a/.env.example b/.env.example index 90070de19..71a9074a6 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,12 @@ # Database Settings -PGUSER="plane" -PGPASSWORD="plane" -PGHOST="plane-db" -PGDATABASE="plane" -DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE} +POSTGRES_USER="plane" +POSTGRES_PASSWORD="plane" +POSTGRES_DB="plane" +PGDATA="/var/lib/postgresql/data" # Redis Settings REDIS_HOST="plane-redis" REDIS_PORT="6379" -REDIS_URL="redis://${REDIS_HOST}:6379/" # AWS Settings AWS_REGION="" diff --git a/.github/ISSUE_TEMPLATE/--bug-report.yaml b/.github/ISSUE_TEMPLATE/--bug-report.yaml index 4240c10c5..5d19be11c 100644 --- a/.github/ISSUE_TEMPLATE/--bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/--bug-report.yaml @@ -1,7 +1,8 @@ name: Bug report description: Create a bug report to help us improve Plane title: "[bug]: " -labels: [bug, need testing] +labels: [🐛bug] +assignees: [srinivaspendem, pushya-plane] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/--feature-request.yaml b/.github/ISSUE_TEMPLATE/--feature-request.yaml index b7ba11679..941fbef87 100644 --- a/.github/ISSUE_TEMPLATE/--feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/--feature-request.yaml @@ -1,7 +1,8 @@ name: Feature request description: Suggest a feature to improve Plane title: "[feature]: " -labels: [feature] +labels: [✨feature] +assignees: [srinivaspendem, pushya-plane] body: - type: markdown attributes: diff --git a/.github/workflows/build-test-pull-request.yml b/.github/workflows/build-test-pull-request.yml index fd5d5ad03..296e965d7 100644 --- a/.github/workflows/build-test-pull-request.yml +++ b/.github/workflows/build-test-pull-request.yml @@ -25,7 +25,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v38 + uses: tj-actions/changed-files@v41 with: files_yaml: | apiserver: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 29fbde453..9f6ab1bfb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,10 +2,10 @@ name: "CodeQL" on: push: - branches: [ 'develop', 'hot-fix', 'stage-release' ] + branches: [ 'develop', 'preview', 'master' ] pull_request: # The branches below must be a subset of the branches above - branches: [ 'develop' ] + branches: [ 'develop', 'preview', 'master' ] schedule: - cron: '53 19 * * 5' diff --git a/.github/workflows/create-sync-pr.yml b/.github/workflows/create-sync-pr.yml index 0f85e940c..add08d1ed 100644 --- a/.github/workflows/create-sync-pr.yml +++ b/.github/workflows/create-sync-pr.yml @@ -3,14 +3,14 @@ name: Create Sync Action on: pull_request: branches: - - develop # Change this to preview + - preview types: - closed -env: +env: SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}} jobs: - create_pr: + sync_changes: # Only run the job when a PR is merged if: github.event.pull_request.merged == true runs-on: ubuntu-latest @@ -33,23 +33,14 @@ jobs: sudo apt update sudo apt install gh -y - - name: Create Pull Request + - name: Push Changes to Target Repo env: GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} run: | TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}" TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}" - TARGET_BASE_BRANCH="${{ secrets.SYNC_TARGET_BASE_BRANCH_NAME }}" SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" git checkout $SOURCE_BRANCH git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git" git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH - - PR_TITLE=${{secrets.SYNC_PR_TITLE}} - - gh pr create \ - --base $TARGET_BASE_BRANCH \ - --head $TARGET_BRANCH \ - --title "$PR_TITLE" \ - --repo $TARGET_REPO diff --git a/README.md b/README.md index 5b96dbf6c..41ebdd169 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Thats it! ## 🍙 Self Hosting -For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page +For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/docker-compose) documentation page ## 🚀 Features diff --git a/apiserver/back_migration.py b/apiserver/back_migration.py index c04ee7771..a0e45416a 100644 --- a/apiserver/back_migration.py +++ b/apiserver/back_migration.py @@ -26,7 +26,9 @@ def update_description(): updated_issues.append(issue) Issue.objects.bulk_update( - updated_issues, ["description_html", "description_stripped"], batch_size=100 + updated_issues, + ["description_html", "description_stripped"], + batch_size=100, ) print("Success") except Exception as e: @@ -40,7 +42,9 @@ def update_comments(): updated_issue_comments = [] for issue_comment in issue_comments: - issue_comment.comment_html = f"

{issue_comment.comment_stripped}

" + issue_comment.comment_html = ( + f"

{issue_comment.comment_stripped}

" + ) updated_issue_comments.append(issue_comment) IssueComment.objects.bulk_update( @@ -99,7 +103,9 @@ def updated_issue_sort_order(): issue.sort_order = issue.sequence_id * random.randint(100, 500) updated_issues.append(issue) - Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100) + Issue.objects.bulk_update( + updated_issues, ["sort_order"], batch_size=100 + ) print("Success") except Exception as e: print(e) @@ -137,7 +143,9 @@ def update_project_cover_images(): project.cover_image = project_cover_images[random.randint(0, 19)] updated_projects.append(project) - Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100) + Project.objects.bulk_update( + updated_projects, ["cover_image"], batch_size=100 + ) print("Success") except Exception as e: print(e) @@ -186,7 +194,9 @@ def update_label_color(): def create_slack_integration(): try: - _ = Integration.objects.create(provider="slack", network=2, title="Slack") + _ = Integration.objects.create( + provider="slack", network=2, title="Slack" + ) print("Success") except Exception as e: print(e) @@ -212,12 +222,16 @@ def update_integration_verified(): def update_start_date(): try: - issues = Issue.objects.filter(state__group__in=["started", "completed"]) + issues = Issue.objects.filter( + state__group__in=["started", "completed"] + ) updated_issues = [] for issue in issues: issue.start_date = issue.created_at.date() updated_issues.append(issue) - Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500) + Issue.objects.bulk_update( + updated_issues, ["start_date"], batch_size=500 + ) print("Success") except Exception as e: print(e) diff --git a/apiserver/manage.py b/apiserver/manage.py index 837297219..744086783 100644 --- a/apiserver/manage.py +++ b/apiserver/manage.py @@ -2,10 +2,10 @@ import os import sys -if __name__ == '__main__': +if __name__ == "__main__": os.environ.setdefault( - 'DJANGO_SETTINGS_MODULE', - 'plane.settings.production') + "DJANGO_SETTINGS_MODULE", "plane.settings.production" + ) try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/apiserver/plane/__init__.py b/apiserver/plane/__init__.py index fb989c4e6..53f4ccb1d 100644 --- a/apiserver/plane/__init__.py +++ b/apiserver/plane/__init__.py @@ -1,3 +1,3 @@ from .celery import app as celery_app -__all__ = ('celery_app',) +__all__ = ("celery_app",) diff --git a/apiserver/plane/analytics/apps.py b/apiserver/plane/analytics/apps.py index 353779983..52a59f313 100644 --- a/apiserver/plane/analytics/apps.py +++ b/apiserver/plane/analytics/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class AnalyticsConfig(AppConfig): - name = 'plane.analytics' + name = "plane.analytics" diff --git a/apiserver/plane/api/apps.py b/apiserver/plane/api/apps.py index 292ad9344..6ba36e7e5 100644 --- a/apiserver/plane/api/apps.py +++ b/apiserver/plane/api/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class ApiConfig(AppConfig): - name = "plane.api" \ No newline at end of file + name = "plane.api" diff --git a/apiserver/plane/api/middleware/api_authentication.py b/apiserver/plane/api/middleware/api_authentication.py index 1b2c03318..893df7f84 100644 --- a/apiserver/plane/api/middleware/api_authentication.py +++ b/apiserver/plane/api/middleware/api_authentication.py @@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication): def validate_api_token(self, token): try: api_token = APIToken.objects.get( - Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)), + Q( + Q(expired_at__gt=timezone.now()) + | Q(expired_at__isnull=True) + ), token=token, is_active=True, ) @@ -44,4 +47,4 @@ class APIKeyAuthentication(authentication.BaseAuthentication): # Validate the API token user, token = self.validate_api_token(token) - return user, token \ No newline at end of file + return user, token diff --git a/apiserver/plane/api/rate_limit.py b/apiserver/plane/api/rate_limit.py index f91e2d65d..b62936d8e 100644 --- a/apiserver/plane/api/rate_limit.py +++ b/apiserver/plane/api/rate_limit.py @@ -1,17 +1,18 @@ from rest_framework.throttling import SimpleRateThrottle + class ApiKeyRateThrottle(SimpleRateThrottle): - scope = 'api_key' - rate = '60/minute' + scope = "api_key" + rate = "60/minute" def get_cache_key(self, request, view): # Retrieve the API key from the request header - api_key = request.headers.get('X-Api-Key') + api_key = request.headers.get("X-Api-Key") if not api_key: return None # Allow the request if there's no API key # Use the API key as part of the cache key - return f'{self.scope}:{api_key}' + return f"{self.scope}:{api_key}" def allow_request(self, request, view): allowed = super().allow_request(request, view) @@ -24,7 +25,7 @@ class ApiKeyRateThrottle(SimpleRateThrottle): # Remove old histories while history and history[-1] <= now - self.duration: history.pop() - + # Calculate the requests num_requests = len(history) @@ -35,7 +36,7 @@ class ApiKeyRateThrottle(SimpleRateThrottle): reset_time = int(now + self.duration) # Add headers - request.META['X-RateLimit-Remaining'] = max(0, available) - request.META['X-RateLimit-Reset'] = reset_time + request.META["X-RateLimit-Remaining"] = max(0, available) + request.META["X-RateLimit-Reset"] = reset_time - return allowed \ No newline at end of file + return allowed diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 1fd1bce78..10b0182d6 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -13,5 +13,9 @@ from .issue import ( ) from .state import StateLiteSerializer, StateSerializer from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer -from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer -from .inbox import InboxIssueSerializer \ No newline at end of file +from .module import ( + ModuleSerializer, + ModuleIssueSerializer, + ModuleLiteSerializer, +) +from .inbox import InboxIssueSerializer diff --git a/apiserver/plane/api/serializers/base.py b/apiserver/plane/api/serializers/base.py index 4e88597c7..da8b96964 100644 --- a/apiserver/plane/api/serializers/base.py +++ b/apiserver/plane/api/serializers/base.py @@ -100,6 +100,8 @@ class BaseSerializer(serializers.ModelSerializer): response[expand] = exp_serializer.data else: # You might need to handle this case differently - response[expand] = getattr(instance, f"{expand}_id", None) + response[expand] = getattr( + instance, f"{expand}_id", None + ) - return response \ No newline at end of file + return response diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index eaff8181a..6fc73a4bc 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -23,7 +23,9 @@ class CycleSerializer(BaseSerializer): and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None) ): - raise serializers.ValidationError("Start date cannot exceed end date") + raise serializers.ValidationError( + "Start date cannot exceed end date" + ) return data class Meta: @@ -55,7 +57,6 @@ class CycleIssueSerializer(BaseSerializer): class CycleLiteSerializer(BaseSerializer): - class Meta: model = Cycle - fields = "__all__" \ No newline at end of file + fields = "__all__" diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/api/serializers/inbox.py index 17ae8c1ed..78bb74d13 100644 --- a/apiserver/plane/api/serializers/inbox.py +++ b/apiserver/plane/api/serializers/inbox.py @@ -2,8 +2,8 @@ from .base import BaseSerializer from plane.db.models import InboxIssue -class InboxIssueSerializer(BaseSerializer): +class InboxIssueSerializer(BaseSerializer): class Meta: model = InboxIssue fields = "__all__" @@ -16,4 +16,4 @@ class InboxIssueSerializer(BaseSerializer): "updated_by", "created_at", "updated_at", - ] \ No newline at end of file + ] diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 75396e9bb..4c8d6e815 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -27,6 +27,7 @@ from .module import ModuleSerializer, ModuleLiteSerializer from .user import UserLiteSerializer from .state import StateLiteSerializer + class IssueSerializer(BaseSerializer): assignees = serializers.ListField( child=serializers.PrimaryKeyRelatedField( @@ -66,14 +67,16 @@ class IssueSerializer(BaseSerializer): and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None) ): - raise serializers.ValidationError("Start date cannot exceed target date") - + raise serializers.ValidationError( + "Start date cannot exceed target date" + ) + try: - if(data.get("description_html", None) is not None): + if data.get("description_html", None) is not None: parsed = html.fromstring(data["description_html"]) - parsed_str = html.tostring(parsed, encoding='unicode') + parsed_str = html.tostring(parsed, encoding="unicode") data["description_html"] = parsed_str - + except Exception as e: raise serializers.ValidationError(f"Invalid HTML: {str(e)}") @@ -96,7 +99,8 @@ class IssueSerializer(BaseSerializer): if ( data.get("state") and not State.objects.filter( - project_id=self.context.get("project_id"), pk=data.get("state").id + project_id=self.context.get("project_id"), + pk=data.get("state").id, ).exists() ): raise serializers.ValidationError( @@ -107,7 +111,8 @@ class IssueSerializer(BaseSerializer): if ( data.get("parent") and not Issue.objects.filter( - workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id + workspace_id=self.context.get("workspace_id"), + pk=data.get("parent").id, ).exists() ): raise serializers.ValidationError( @@ -238,9 +243,13 @@ class IssueSerializer(BaseSerializer): ] if "labels" in self.fields: if "labels" in self.expand: - data["labels"] = LabelSerializer(instance.labels.all(), many=True).data + data["labels"] = LabelSerializer( + instance.labels.all(), many=True + ).data else: - data["labels"] = [str(label.id) for label in instance.labels.all()] + data["labels"] = [ + str(label.id) for label in instance.labels.all() + ] return data @@ -278,7 +287,8 @@ class IssueLinkSerializer(BaseSerializer): # Validation if url already exists def create(self, validated_data): if IssueLink.objects.filter( - url=validated_data.get("url"), issue_id=validated_data.get("issue_id") + url=validated_data.get("url"), + issue_id=validated_data.get("issue_id"), ).exists(): raise serializers.ValidationError( {"error": "URL already exists for this Issue"} @@ -324,11 +334,11 @@ class IssueCommentSerializer(BaseSerializer): def validate(self, data): try: - if(data.get("comment_html", None) is not None): + if data.get("comment_html", None) is not None: parsed = html.fromstring(data["comment_html"]) - parsed_str = html.tostring(parsed, encoding='unicode') + parsed_str = html.tostring(parsed, encoding="unicode") data["comment_html"] = parsed_str - + except Exception as e: raise serializers.ValidationError(f"Invalid HTML: {str(e)}") return data @@ -362,7 +372,6 @@ class ModuleIssueSerializer(BaseSerializer): class LabelLiteSerializer(BaseSerializer): - class Meta: model = Label fields = [ diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py index a96a9b54d..01a201064 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -52,7 +52,9 @@ class ModuleSerializer(BaseSerializer): and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None) ): - raise serializers.ValidationError("Start date cannot exceed target date") + raise serializers.ValidationError( + "Start date cannot exceed target date" + ) if data.get("members", []): data["members"] = ProjectMember.objects.filter( @@ -146,16 +148,16 @@ class ModuleLinkSerializer(BaseSerializer): # Validation if url already exists def create(self, validated_data): if ModuleLink.objects.filter( - url=validated_data.get("url"), module_id=validated_data.get("module_id") + url=validated_data.get("url"), + module_id=validated_data.get("module_id"), ).exists(): raise serializers.ValidationError( {"error": "URL already exists for this Issue"} ) return ModuleLink.objects.create(**validated_data) - + class ModuleLiteSerializer(BaseSerializer): - class Meta: model = Module - fields = "__all__" \ No newline at end of file + fields = "__all__" diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index c394a080d..342cc1a81 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -2,12 +2,17 @@ from rest_framework import serializers # Module imports -from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate +from plane.db.models import ( + Project, + ProjectIdentifier, + WorkspaceMember, + State, + Estimate, +) from .base import BaseSerializer class ProjectSerializer(BaseSerializer): - total_members = serializers.IntegerField(read_only=True) total_cycles = serializers.IntegerField(read_only=True) total_modules = serializers.IntegerField(read_only=True) @@ -21,7 +26,7 @@ class ProjectSerializer(BaseSerializer): fields = "__all__" read_only_fields = [ "id", - 'emoji', + "emoji", "workspace", "created_at", "updated_at", @@ -59,12 +64,16 @@ class ProjectSerializer(BaseSerializer): def create(self, validated_data): identifier = validated_data.get("identifier", "").strip().upper() if identifier == "": - raise serializers.ValidationError(detail="Project Identifier is required") + raise serializers.ValidationError( + detail="Project Identifier is required" + ) if ProjectIdentifier.objects.filter( name=identifier, workspace_id=self.context["workspace_id"] ).exists(): - raise serializers.ValidationError(detail="Project Identifier is taken") + raise serializers.ValidationError( + detail="Project Identifier is taken" + ) project = Project.objects.create( **validated_data, workspace_id=self.context["workspace_id"] @@ -89,4 +98,4 @@ class ProjectLiteSerializer(BaseSerializer): "emoji", "description", ] - read_only_fields = fields \ No newline at end of file + read_only_fields = fields diff --git a/apiserver/plane/api/serializers/state.py b/apiserver/plane/api/serializers/state.py index 9d08193d8..1649a7bcf 100644 --- a/apiserver/plane/api/serializers/state.py +++ b/apiserver/plane/api/serializers/state.py @@ -7,9 +7,9 @@ class StateSerializer(BaseSerializer): def validate(self, data): # If the default is being provided then make all other states default False if data.get("default", False): - State.objects.filter(project_id=self.context.get("project_id")).update( - default=False - ) + State.objects.filter( + project_id=self.context.get("project_id") + ).update(default=False) return data class Meta: @@ -35,4 +35,4 @@ class StateLiteSerializer(BaseSerializer): "color", "group", ] - read_only_fields = fields \ No newline at end of file + read_only_fields = fields diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index 42b6c3967..fe50021b5 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -13,4 +13,4 @@ class UserLiteSerializer(BaseSerializer): "avatar", "display_name", ] - read_only_fields = fields \ No newline at end of file + read_only_fields = fields diff --git a/apiserver/plane/api/serializers/workspace.py b/apiserver/plane/api/serializers/workspace.py index c4c5caceb..a47de3d31 100644 --- a/apiserver/plane/api/serializers/workspace.py +++ b/apiserver/plane/api/serializers/workspace.py @@ -5,6 +5,7 @@ from .base import BaseSerializer class WorkspaceLiteSerializer(BaseSerializer): """Lite serializer with only required fields""" + class Meta: model = Workspace fields = [ @@ -12,4 +13,4 @@ class WorkspaceLiteSerializer(BaseSerializer): "slug", "id", ] - read_only_fields = fields \ No newline at end of file + read_only_fields = fields diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py index a5ef0f5f1..84927439e 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/api/urls/__init__.py @@ -12,4 +12,4 @@ urlpatterns = [ *cycle_patterns, *module_patterns, *inbox_patterns, -] \ No newline at end of file +] diff --git a/apiserver/plane/api/urls/cycle.py b/apiserver/plane/api/urls/cycle.py index f557f8af0..593e501bf 100644 --- a/apiserver/plane/api/urls/cycle.py +++ b/apiserver/plane/api/urls/cycle.py @@ -32,4 +32,4 @@ urlpatterns = [ TransferCycleIssueAPIEndpoint.as_view(), name="transfer-issues", ), -] \ No newline at end of file +] diff --git a/apiserver/plane/api/urls/inbox.py b/apiserver/plane/api/urls/inbox.py index 3a2a57786..95eb68f3f 100644 --- a/apiserver/plane/api/urls/inbox.py +++ b/apiserver/plane/api/urls/inbox.py @@ -14,4 +14,4 @@ urlpatterns = [ InboxIssueAPIEndpoint.as_view(), name="inbox-issue", ), -] \ No newline at end of file +] diff --git a/apiserver/plane/api/urls/module.py b/apiserver/plane/api/urls/module.py index 7117a9e8b..4309f44e9 100644 --- a/apiserver/plane/api/urls/module.py +++ b/apiserver/plane/api/urls/module.py @@ -23,4 +23,4 @@ urlpatterns = [ ModuleIssueAPIEndpoint.as_view(), name="module-issues", ), -] \ No newline at end of file +] diff --git a/apiserver/plane/api/urls/project.py b/apiserver/plane/api/urls/project.py index c73e84c89..1ed450c86 100644 --- a/apiserver/plane/api/urls/project.py +++ b/apiserver/plane/api/urls/project.py @@ -3,7 +3,7 @@ from django.urls import path from plane.api.views import ProjectAPIEndpoint urlpatterns = [ - path( + path( "workspaces//projects/", ProjectAPIEndpoint.as_view(), name="project", @@ -13,4 +13,4 @@ urlpatterns = [ ProjectAPIEndpoint.as_view(), name="project", ), -] \ No newline at end of file +] diff --git a/apiserver/plane/api/urls/state.py b/apiserver/plane/api/urls/state.py index 0676ac5ad..b03f386e6 100644 --- a/apiserver/plane/api/urls/state.py +++ b/apiserver/plane/api/urls/state.py @@ -13,4 +13,4 @@ urlpatterns = [ StateAPIEndpoint.as_view(), name="states", ), -] \ No newline at end of file +] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 84d8dcabb..0da79566f 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -18,4 +18,4 @@ from .cycle import ( from .module import ModuleAPIEndpoint, ModuleIssueAPIEndpoint -from .inbox import InboxIssueAPIEndpoint \ No newline at end of file +from .inbox import InboxIssueAPIEndpoint diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index abde4e8b0..b069ef78c 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -41,7 +41,9 @@ class WebhookMixin: bulk = False def finalize_response(self, request, response, *args, **kwargs): - response = super().finalize_response(request, response, *args, **kwargs) + response = super().finalize_response( + request, response, *args, **kwargs + ) # Check for the case should webhook be sent if ( @@ -104,15 +106,14 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): ) if isinstance(e, ObjectDoesNotExist): - model_name = str(exc).split(" matching query does not exist.")[0] return Response( - {"error": f"{model_name} does not exist."}, + {"error": f"The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) if isinstance(e, KeyError): return Response( - {"error": f"key {e} does not exist"}, + {"error": f" The required key does not exist."}, status=status.HTTP_400_BAD_REQUEST, ) @@ -140,7 +141,9 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): def finalize_response(self, request, response, *args, **kwargs): # Call super to get the default response - response = super().finalize_response(request, response, *args, **kwargs) + response = super().finalize_response( + request, response, *args, **kwargs + ) # Add custom headers if they exist in the request META ratelimit_remaining = request.META.get("X-RateLimit-Remaining") @@ -164,13 +167,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): @property def fields(self): fields = [ - field for field in self.request.GET.get("fields", "").split(",") if field + field + for field in self.request.GET.get("fields", "").split(",") + if field ] return fields if fields else None @property def expand(self): expand = [ - expand for expand in self.request.GET.get("expand", "").split(",") if expand + expand + for expand in self.request.GET.get("expand", "").split(",") + if expand ] return expand if expand else None diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 310332333..c296bb111 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -12,7 +12,13 @@ from rest_framework import status # Module imports from .base import BaseAPIView, WebhookMixin -from plane.db.models import Cycle, Issue, CycleIssue, IssueLink, IssueAttachment +from plane.db.models import ( + Cycle, + Issue, + CycleIssue, + IssueLink, + IssueAttachment, +) from plane.app.permissions import ProjectEntityPermission from plane.api.serializers import ( CycleSerializer, @@ -102,7 +108,9 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView): ), ) ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) + .annotate( + total_estimates=Sum("issue_cycle__issue__estimate_point") + ) .annotate( completed_estimates=Sum( "issue_cycle__issue__estimate_point", @@ -201,7 +209,8 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView): # Incomplete Cycles if cycle_view == "incomplete": queryset = queryset.filter( - Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True), + Q(end_date__gte=timezone.now().date()) + | Q(end_date__isnull=True), ) return self.paginate( request=request, @@ -238,8 +247,12 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView): project_id=project_id, owned_by=request.user, ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) else: return Response( { @@ -249,15 +262,22 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView): ) def patch(self, request, slug, project_id, pk): - cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) request_data = request.data - if cycle.end_date is not None and cycle.end_date < timezone.now().date(): + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): if "sort_order" in request_data: # Can only change sort order request_data = { - "sort_order": request_data.get("sort_order", cycle.sort_order) + "sort_order": request_data.get( + "sort_order", cycle.sort_order + ) } else: return Response( @@ -275,11 +295,13 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView): def delete(self, request, slug, project_id, pk): cycle_issues = list( - CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list( - "issue", flat=True - ) + CycleIssue.objects.filter( + cycle_id=self.kwargs.get("pk") + ).values_list("issue", flat=True) + ) + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk ) - cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) issue_activity.delay( type="cycle.activity.deleted", @@ -319,7 +341,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): def get_queryset(self): return ( CycleIssue.objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("issue_id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -342,7 +366,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): issues = ( Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -364,7 +390,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -387,14 +415,18 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): if not issues: return Response( - {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, ) cycle = Cycle.objects.get( workspace__slug=slug, project_id=project_id, pk=cycle_id ) - if cycle.end_date is not None and cycle.end_date < timezone.now().date(): + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): return Response( { "error": "The Cycle has already been completed so no new issues can be added" @@ -479,7 +511,10 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): def delete(self, request, slug, project_id, cycle_id, issue_id): cycle_issue = CycleIssue.objects.get( - issue_id=issue_id, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id + issue_id=issue_id, + workspace__slug=slug, + project_id=project_id, + cycle_id=cycle_id, ) issue_id = cycle_issue.issue_id cycle_issue.delete() @@ -550,4 +585,4 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): updated_cycles, ["cycle_id"], batch_size=100 ) - return Response({"message": "Success"}, status=status.HTTP_200_OK) \ No newline at end of file + return Response({"message": "Success"}, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 4f4cdc4ef..c1079345a 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -14,7 +14,14 @@ from rest_framework.response import Response from .base import BaseAPIView from plane.app.permissions import ProjectLitePermission from plane.api.serializers import InboxIssueSerializer, IssueSerializer -from plane.db.models import InboxIssue, Issue, State, ProjectMember, Project, Inbox +from plane.db.models import ( + InboxIssue, + Issue, + State, + ProjectMember, + Project, + Inbox, +) from plane.bgtasks.issue_activites_task import issue_activity @@ -43,7 +50,8 @@ class InboxIssueAPIEndpoint(BaseAPIView): ).first() project = Project.objects.get( - workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id") + workspace__slug=self.kwargs.get("slug"), + pk=self.kwargs.get("project_id"), ) if inbox is None and not project.inbox_view: @@ -51,7 +59,8 @@ class InboxIssueAPIEndpoint(BaseAPIView): return ( InboxIssue.objects.filter( - Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + Q(snoozed_till__gte=timezone.now()) + | Q(snoozed_till__isnull=True), workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"), inbox_id=inbox.id, @@ -87,7 +96,8 @@ class InboxIssueAPIEndpoint(BaseAPIView): def post(self, request, slug, project_id): if not request.data.get("issue", {}).get("name", False): return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, ) inbox = Inbox.objects.filter( @@ -117,7 +127,8 @@ class InboxIssueAPIEndpoint(BaseAPIView): "none", ]: return Response( - {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Invalid priority"}, + status=status.HTTP_400_BAD_REQUEST, ) # Create or get state @@ -222,10 +233,14 @@ class InboxIssueAPIEndpoint(BaseAPIView): "description_html": issue_data.get( "description_html", issue.description_html ), - "description": issue_data.get("description", issue.description), + "description": issue_data.get( + "description", issue.description + ), } - issue_serializer = IssueSerializer(issue, data=issue_data, partial=True) + issue_serializer = IssueSerializer( + issue, data=issue_data, partial=True + ) if issue_serializer.is_valid(): current_instance = issue @@ -266,7 +281,9 @@ class InboxIssueAPIEndpoint(BaseAPIView): project_id=project_id, ) state = State.objects.filter( - group="cancelled", workspace__slug=slug, project_id=project_id + group="cancelled", + workspace__slug=slug, + project_id=project_id, ).first() if state is not None: issue.state = state @@ -284,17 +301,22 @@ class InboxIssueAPIEndpoint(BaseAPIView): if issue.state.name == "Triage": # Move to default state state = State.objects.filter( - workspace__slug=slug, project_id=project_id, default=True + workspace__slug=slug, + project_id=project_id, + default=True, ).first() if state is not None: issue.state = state issue.save() return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) else: return Response( - InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK + InboxIssueSerializer(inbox_issue).data, + status=status.HTTP_200_OK, ) def delete(self, request, slug, project_id, issue_id): diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 1ac8ddcff..e91f2a5f6 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -67,7 +67,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): def get_queryset(self): return ( Issue.issue_objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -86,7 +88,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): def get(self, request, slug, project_id, pk=None): if pk: issue = Issue.issue_objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -102,7 +106,13 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] order_by_param = request.GET.get("order_by", "-created_at") @@ -117,7 +127,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -127,7 +139,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( - priority_order if order_by_param == "priority" else priority_order[::-1] + priority_order + if order_by_param == "priority" + else priority_order[::-1] ) issue_queryset = issue_queryset.annotate( priority_order=Case( @@ -175,7 +189,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): else order_by_param ) ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + "-max_values" + if order_by_param.startswith("-") + else "max_values" ) else: issue_queryset = issue_queryset.order_by(order_by_param) @@ -209,7 +225,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): # Track the issue issue_activity.delay( type="issue.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), actor_id=str(request.user.id), issue_id=str(serializer.data.get("id", None)), project_id=str(project_id), @@ -220,7 +238,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def patch(self, request, slug, project_id, pk=None): - issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) project = Project.objects.get(pk=project_id) current_instance = json.dumps( IssueSerializer(issue).data, cls=DjangoJSONEncoder @@ -250,7 +270,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, slug, project_id, pk=None): - issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) current_instance = json.dumps( IssueSerializer(issue).data, cls=DjangoJSONEncoder ) @@ -297,11 +319,17 @@ class LabelAPIEndpoint(BaseAPIView): serializer = LabelSerializer(data=request.data) if serializer.is_valid(): serializer.save(project_id=project_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) except IntegrityError: return Response( - {"error": "Label with the same name already exists in the project"}, + { + "error": "Label with the same name already exists in the project" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -318,7 +346,11 @@ class LabelAPIEndpoint(BaseAPIView): ).data, ) label = self.get_queryset().get(pk=pk) - serializer = LabelSerializer(label, fields=self.fields, expand=self.expand,) + serializer = LabelSerializer( + label, + fields=self.fields, + expand=self.expand, + ) return Response(serializer.data, status=status.HTTP_200_OK) def patch(self, request, slug, project_id, pk=None): @@ -328,7 +360,6 @@ class LabelAPIEndpoint(BaseAPIView): 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, project_id, pk=None): label = self.get_queryset().get(pk=pk) @@ -395,7 +426,9 @@ class IssueLinkAPIEndpoint(BaseAPIView): ) issue_activity.delay( type="link.activity.created", - requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id")), project_id=str(self.kwargs.get("project_id")), @@ -407,14 +440,19 @@ class IssueLinkAPIEndpoint(BaseAPIView): def patch(self, request, slug, project_id, issue_id, pk): issue_link = IssueLink.objects.get( - workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, ) requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) current_instance = json.dumps( IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder, ) - serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True) + serializer = IssueLinkSerializer( + issue_link, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() issue_activity.delay( @@ -431,7 +469,10 @@ class IssueLinkAPIEndpoint(BaseAPIView): def delete(self, request, slug, project_id, issue_id, pk): issue_link = IssueLink.objects.get( - workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, ) current_instance = json.dumps( IssueLinkSerializer(issue_link).data, @@ -466,7 +507,9 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): def get_queryset(self): return ( - IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug")) + IssueComment.objects.filter( + workspace__slug=self.kwargs.get("slug") + ) .filter(project_id=self.kwargs.get("project_id")) .filter(issue_id=self.kwargs.get("issue_id")) .filter(project__project_projectmember__member=self.request.user) @@ -518,7 +561,9 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): ) issue_activity.delay( type="comment.activity.created", - requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id")), project_id=str(self.kwargs.get("project_id")), @@ -530,7 +575,10 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): def patch(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 + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, ) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) current_instance = json.dumps( @@ -556,7 +604,10 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): 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 + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, ) current_instance = json.dumps( IssueCommentSerializer(issue_comment).data, @@ -591,7 +642,7 @@ class IssueActivityAPIEndpoint(BaseAPIView): ) .select_related("actor", "workspace", "issue", "project") ).order_by(request.GET.get("order_by", "created_at")) - + if pk: issue_activities = issue_activities.get(pk=pk) serializer = IssueActivitySerializer(issue_activities) diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 959b7ccc3..1a9a21a3c 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -55,7 +55,9 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): .prefetch_related( Prefetch( "link_module", - queryset=ModuleLink.objects.select_related("module", "created_by"), + queryset=ModuleLink.objects.select_related( + "module", "created_by" + ), ) ) .annotate( @@ -122,17 +124,30 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): def post(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) - serializer = ModuleSerializer(data=request.data, context={"project_id": project_id, "workspace_id": project.workspace_id}) + serializer = ModuleSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + }, + ) if serializer.is_valid(): serializer.save() module = Module.objects.get(pk=serializer.data["id"]) serializer = ModuleSerializer(module) 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, pk): - module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug) - serializer = ModuleSerializer(module, data=request.data, context={"project_id": project_id}, partial=True) + module = Module.objects.get( + pk=pk, project_id=project_id, workspace__slug=slug + ) + serializer = ModuleSerializer( + module, + data=request.data, + context={"project_id": project_id}, + partial=True, + ) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -162,9 +177,13 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): ) def delete(self, request, slug, project_id, pk): - module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + module = Module.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) module_issues = list( - ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True) + ModuleIssue.objects.filter(module_id=pk).values_list( + "issue", flat=True + ) ) issue_activity.delay( type="module.activity.deleted", @@ -204,7 +223,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): def get_queryset(self): return ( ModuleIssue.objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("issue") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -228,7 +249,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): issues = ( Issue.issue_objects.filter(issue_module__module_id=module_id) .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -250,7 +273,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -271,7 +296,8 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): issues = request.data.get("issues", []) if not len(issues): return Response( - {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, ) module = Module.objects.get( workspace__slug=slug, project_id=project_id, pk=module_id @@ -354,7 +380,10 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): def delete(self, request, slug, project_id, module_id, issue_id): module_issue = ModuleIssue.objects.get( - workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id + workspace__slug=slug, + project_id=project_id, + module_id=module_id, + issue_id=issue_id, ) module_issue.delete() issue_activity.delay( @@ -371,4 +400,4 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): current_instance=None, epoch=int(timezone.now().timestamp()), ) - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index e8dc9f5a9..cb1f7dc7b 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -39,9 +39,15 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): def get_queryset(self): return ( Project.objects.filter(workspace__slug=self.kwargs.get("slug")) - .filter(Q(project_projectmember__member=self.request.user) | Q(network=2)) + .filter( + Q(project_projectmember__member=self.request.user) + | Q(network=2) + ) .select_related( - "workspace", "workspace__owner", "default_assignee", "project_lead" + "workspace", + "workspace__owner", + "default_assignee", + "project_lead", ) .annotate( is_member=Exists( @@ -120,11 +126,18 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): request=request, queryset=(projects), on_results=lambda projects: ProjectSerializer( - projects, many=True, fields=self.fields, expand=self.expand, + projects, + many=True, + fields=self.fields, + expand=self.expand, ).data, ) project = self.get_queryset().get(workspace__slug=slug, pk=project_id) - serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand,) + serializer = ProjectSerializer( + project, + fields=self.fields, + expand=self.expand, + ) return Response(serializer.data, status=status.HTTP_200_OK) def post(self, request, slug): @@ -138,7 +151,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): # Add the user as Administrator to the project project_member = ProjectMember.objects.create( - project_id=serializer.data["id"], member=request.user, role=20 + project_id=serializer.data["id"], + member=request.user, + role=20, ) # Also create the issue property for the user _ = IssueProperty.objects.create( @@ -211,9 +226,15 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): ] ) - project = self.get_queryset().filter(pk=serializer.data["id"]).first() + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) serializer = ProjectSerializer(project) - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST, @@ -226,7 +247,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): ) except Workspace.DoesNotExist as e: return Response( - {"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Workspace does not exist"}, + status=status.HTTP_404_NOT_FOUND, ) except ValidationError as e: return Response( @@ -250,7 +272,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): serializer.save() if serializer.data["inbox_view"]: Inbox.objects.get_or_create( - name=f"{project.name} Inbox", project=project, is_default=True + name=f"{project.name} Inbox", + project=project, + is_default=True, ) # Create the triage state in Backlog group @@ -262,10 +286,16 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): color="#ff7700", ) - project = self.get_queryset().filter(pk=serializer.data["id"]).first() + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) serializer = ProjectSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) except IntegrityError as e: if "already exists" in str(e): return Response( @@ -274,7 +304,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): ) except (Project.DoesNotExist, Workspace.DoesNotExist): return Response( - {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Project does not exist"}, + status=status.HTTP_404_NOT_FOUND, ) except ValidationError as e: return Response( @@ -285,4 +316,4 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): def delete(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 3d2861778..f931c2ed2 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -34,7 +34,9 @@ class StateAPIEndpoint(BaseAPIView): ) def post(self, request, slug, project_id): - serializer = StateSerializer(data=request.data, context={"project_id": project_id}) + serializer = StateSerializer( + data=request.data, context={"project_id": project_id} + ) if serializer.is_valid(): serializer.save(project_id=project_id) return Response(serializer.data, status=status.HTTP_200_OK) @@ -64,14 +66,19 @@ class StateAPIEndpoint(BaseAPIView): ) if state.default: - return Response({"error": "Default state cannot be deleted"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": "Default state cannot be deleted"}, + status=status.HTTP_400_BAD_REQUEST, + ) # Check for any issues in the state issue_exist = Issue.issue_objects.filter(state=state_id).exists() if issue_exist: return Response( - {"error": "The state is not empty, only empty states can be deleted"}, + { + "error": "The state is not empty, only empty states can be deleted" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -79,9 +86,11 @@ class StateAPIEndpoint(BaseAPIView): return Response(status=status.HTTP_204_NO_CONTENT) def patch(self, request, slug, project_id, state_id=None): - state = State.objects.get(workspace__slug=slug, project_id=project_id, pk=state_id) + state = State.objects.get( + workspace__slug=slug, project_id=project_id, pk=state_id + ) serializer = StateSerializer(state, data=request.data, partial=True) 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) \ No newline at end of file + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/app/middleware/api_authentication.py b/apiserver/plane/app/middleware/api_authentication.py index ddabb4132..893df7f84 100644 --- a/apiserver/plane/app/middleware/api_authentication.py +++ b/apiserver/plane/app/middleware/api_authentication.py @@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication): def validate_api_token(self, token): try: api_token = APIToken.objects.get( - Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)), + Q( + Q(expired_at__gt=timezone.now()) + | Q(expired_at__isnull=True) + ), token=token, is_active=True, ) diff --git a/apiserver/plane/app/permissions/__init__.py b/apiserver/plane/app/permissions/__init__.py index 2298f3442..8e8793504 100644 --- a/apiserver/plane/app/permissions/__init__.py +++ b/apiserver/plane/app/permissions/__init__.py @@ -1,4 +1,3 @@ - from .workspace import ( WorkSpaceBasePermission, WorkspaceOwnerPermission, @@ -13,5 +12,3 @@ from .project import ( ProjectMemberPermission, ProjectLitePermission, ) - - diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 4e0c12fe5..094328fff 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -35,7 +35,11 @@ from .project import ( ProjectMemberRoleSerializer, ) from .state import StateSerializer, StateLiteSerializer -from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer +from .view import ( + GlobalViewSerializer, + IssueViewSerializer, + IssueViewFavoriteSerializer, +) from .cycle import ( CycleSerializer, CycleIssueSerializer, @@ -64,8 +68,6 @@ from .issue import ( IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, - IssueRelationLiteSerializer, - ) from .module import ( @@ -91,7 +93,12 @@ from .integration import ( from .importer import ImporterSerializer -from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer +from .page import ( + PageSerializer, + PageLogSerializer, + SubPageSerializer, + PageFavoriteSerializer, +) from .estimate import ( EstimateSerializer, @@ -99,7 +106,11 @@ from .estimate import ( EstimateReadSerializer, ) -from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer +from .inbox import ( + InboxSerializer, + InboxIssueSerializer, + IssueStateInboxSerializer, +) from .analytic import AnalyticViewSerializer @@ -107,4 +118,6 @@ from .notification import NotificationSerializer from .exporter import ExporterHistorySerializer -from .webhook import WebhookSerializer, WebhookLogSerializer \ No newline at end of file +from .webhook import WebhookSerializer, WebhookLogSerializer + +from .dashboard import DashboardSerializer, WidgetSerializer diff --git a/apiserver/plane/app/serializers/api.py b/apiserver/plane/app/serializers/api.py index 08bb747d9..264a58f92 100644 --- a/apiserver/plane/app/serializers/api.py +++ b/apiserver/plane/app/serializers/api.py @@ -3,7 +3,6 @@ from plane.db.models import APIToken, APIActivityLog class APITokenSerializer(BaseSerializer): - class Meta: model = APIToken fields = "__all__" @@ -18,14 +17,12 @@ class APITokenSerializer(BaseSerializer): class APITokenReadSerializer(BaseSerializer): - class Meta: model = APIToken - exclude = ('token',) + exclude = ("token",) class APIActivityLogSerializer(BaseSerializer): - class Meta: model = APIActivityLog fields = "__all__" diff --git a/apiserver/plane/app/serializers/base.py b/apiserver/plane/app/serializers/base.py index f67f5cf52..89683ffe5 100644 --- a/apiserver/plane/app/serializers/base.py +++ b/apiserver/plane/app/serializers/base.py @@ -4,8 +4,8 @@ from rest_framework import serializers class BaseSerializer(serializers.ModelSerializer): id = serializers.PrimaryKeyRelatedField(read_only=True) -class DynamicBaseSerializer(BaseSerializer): +class DynamicBaseSerializer(BaseSerializer): def __init__(self, *args, **kwargs): # If 'fields' is provided in the arguments, remove it and store it separately. # This is done so as not to pass this custom argument up to the superclass. @@ -32,7 +32,7 @@ class DynamicBaseSerializer(BaseSerializer): # loop through its keys and values. if isinstance(field_name, dict): for key, value in field_name.items(): - # If the value of this nested field is a list, + # If the value of this nested field is a list, # perform a recursive filter on it. if isinstance(value, list): self._filter_fields(self.fields[key], value) @@ -59,6 +59,7 @@ class DynamicBaseSerializer(BaseSerializer): LabelSerializer, CycleIssueSerializer, IssueFlatSerializer, + IssueRelationSerializer, ) # Expansion mapper @@ -77,14 +78,14 @@ class DynamicBaseSerializer(BaseSerializer): "assignees": UserLiteSerializer, "labels": LabelSerializer, "issue_cycle": CycleIssueSerializer, - "parent": IssueFlatSerializer, + "parent": IssueSerializer, + "issue_relation": IssueRelationSerializer, } - self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle"] else False) + self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation"] else False) return self.fields - def to_representation(self, instance): response = super().to_representation(instance) @@ -101,6 +102,7 @@ class DynamicBaseSerializer(BaseSerializer): IssueSerializer, LabelSerializer, CycleIssueSerializer, + IssueRelationSerializer, ) # Expansion mapper @@ -119,6 +121,8 @@ class DynamicBaseSerializer(BaseSerializer): "assignees": UserLiteSerializer, "labels": LabelSerializer, "issue_cycle": CycleIssueSerializer, + "parent": IssueSerializer, + "issue_relation": IssueRelationSerializer } # Check if field in expansion then expand the field if expand in expansion: @@ -133,6 +137,8 @@ class DynamicBaseSerializer(BaseSerializer): response[expand] = exp_serializer.data else: # You might need to handle this case differently - response[expand] = getattr(instance, f"{expand}_id", None) + response[expand] = getattr( + instance, f"{expand}_id", None + ) return response diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index f0ee8f9da..a041dd227 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -7,7 +7,12 @@ from .user import UserLiteSerializer from .issue import IssueStateSerializer from .workspace import WorkspaceLiteSerializer from .project import ProjectLiteSerializer -from plane.db.models import Cycle, CycleIssue, CycleFavorite, CycleUserProperties +from plane.db.models import ( + Cycle, + CycleIssue, + CycleFavorite, + CycleUserProperties, +) class CycleWriteSerializer(BaseSerializer): @@ -17,7 +22,9 @@ class CycleWriteSerializer(BaseSerializer): and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None) ): - raise serializers.ValidationError("Start date cannot exceed end date") + raise serializers.ValidationError( + "Start date cannot exceed end date" + ) return data class Meta: @@ -38,7 +45,9 @@ class CycleSerializer(BaseSerializer): total_estimates = serializers.IntegerField(read_only=True) completed_estimates = serializers.IntegerField(read_only=True) started_estimates = serializers.IntegerField(read_only=True) - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + workspace_detail = WorkspaceLiteSerializer( + read_only=True, source="workspace" + ) project_detail = ProjectLiteSerializer(read_only=True, source="project") status = serializers.CharField(read_only=True) @@ -48,7 +57,9 @@ class CycleSerializer(BaseSerializer): and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None) ): - raise serializers.ValidationError("Start date cannot exceed end date") + raise serializers.ValidationError( + "Start date cannot exceed end date" + ) return data def get_assignees(self, obj): @@ -115,6 +126,5 @@ class CycleUserPropertiesSerializer(BaseSerializer): read_only_fields = [ "workspace", "project", - "cycle" - "user", - ] \ No newline at end of file + "cycle" "user", + ] diff --git a/apiserver/plane/app/serializers/dashboard.py b/apiserver/plane/app/serializers/dashboard.py new file mode 100644 index 000000000..8fca3c906 --- /dev/null +++ b/apiserver/plane/app/serializers/dashboard.py @@ -0,0 +1,26 @@ +# Module imports +from .base import BaseSerializer +from plane.db.models import Dashboard, Widget + +# Third party frameworks +from rest_framework import serializers + + +class DashboardSerializer(BaseSerializer): + class Meta: + model = Dashboard + fields = "__all__" + + +class WidgetSerializer(BaseSerializer): + is_visible = serializers.BooleanField(read_only=True) + widget_filters = serializers.JSONField(read_only=True) + + class Meta: + model = Widget + fields = [ + "id", + "key", + "is_visible", + "widget_filters" + ] \ No newline at end of file diff --git a/apiserver/plane/app/serializers/estimate.py b/apiserver/plane/app/serializers/estimate.py index 2c2f26e4e..a563c6956 100644 --- a/apiserver/plane/app/serializers/estimate.py +++ b/apiserver/plane/app/serializers/estimate.py @@ -2,12 +2,18 @@ from .base import BaseSerializer from plane.db.models import Estimate, EstimatePoint -from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer +from plane.app.serializers import ( + WorkspaceLiteSerializer, + ProjectLiteSerializer, +) from rest_framework import serializers + class EstimateSerializer(BaseSerializer): - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + workspace_detail = WorkspaceLiteSerializer( + read_only=True, source="workspace" + ) project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: @@ -20,13 +26,14 @@ class EstimateSerializer(BaseSerializer): class EstimatePointSerializer(BaseSerializer): - def validate(self, data): if not data: raise serializers.ValidationError("Estimate points are required") value = data.get("value") if value and len(value) > 20: - raise serializers.ValidationError("Value can't be more than 20 characters") + raise serializers.ValidationError( + "Value can't be more than 20 characters" + ) return data class Meta: @@ -41,7 +48,9 @@ class EstimatePointSerializer(BaseSerializer): class EstimateReadSerializer(BaseSerializer): points = EstimatePointSerializer(read_only=True, many=True) - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + workspace_detail = WorkspaceLiteSerializer( + read_only=True, source="workspace" + ) project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: diff --git a/apiserver/plane/app/serializers/exporter.py b/apiserver/plane/app/serializers/exporter.py index 5c78cfa69..2dd850fd3 100644 --- a/apiserver/plane/app/serializers/exporter.py +++ b/apiserver/plane/app/serializers/exporter.py @@ -5,7 +5,9 @@ from .user import UserLiteSerializer class ExporterHistorySerializer(BaseSerializer): - initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True) + initiated_by_detail = UserLiteSerializer( + source="initiated_by", read_only=True + ) class Meta: model = ExporterHistory diff --git a/apiserver/plane/app/serializers/importer.py b/apiserver/plane/app/serializers/importer.py index 8997f6392..c058994d6 100644 --- a/apiserver/plane/app/serializers/importer.py +++ b/apiserver/plane/app/serializers/importer.py @@ -7,9 +7,13 @@ from plane.db.models import Importer class ImporterSerializer(BaseSerializer): - initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True) + initiated_by_detail = UserLiteSerializer( + source="initiated_by", read_only=True + ) project_detail = ProjectLiteSerializer(source="project", read_only=True) - workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + workspace_detail = WorkspaceLiteSerializer( + source="workspace", read_only=True + ) class Meta: model = Importer diff --git a/apiserver/plane/app/serializers/inbox.py b/apiserver/plane/app/serializers/inbox.py index cdc2646dd..1dc6f1f4a 100644 --- a/apiserver/plane/app/serializers/inbox.py +++ b/apiserver/plane/app/serializers/inbox.py @@ -46,8 +46,12 @@ class InboxIssueLiteSerializer(BaseSerializer): class IssueStateInboxSerializer(BaseSerializer): state_detail = StateLiteSerializer(read_only=True, source="state") project_detail = ProjectLiteSerializer(read_only=True, source="project") - label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) - assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) + label_details = LabelLiteSerializer( + read_only=True, source="labels", many=True + ) + assignee_details = UserLiteSerializer( + read_only=True, source="assignees", many=True + ) sub_issues_count = serializers.IntegerField(read_only=True) issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True) diff --git a/apiserver/plane/app/serializers/integration/base.py b/apiserver/plane/app/serializers/integration/base.py index 6f6543b9e..01e484ed0 100644 --- a/apiserver/plane/app/serializers/integration/base.py +++ b/apiserver/plane/app/serializers/integration/base.py @@ -13,7 +13,9 @@ class IntegrationSerializer(BaseSerializer): class WorkspaceIntegrationSerializer(BaseSerializer): - integration_detail = IntegrationSerializer(read_only=True, source="integration") + integration_detail = IntegrationSerializer( + read_only=True, source="integration" + ) class Meta: model = WorkspaceIntegration diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 6d39f1760..0b3b666ce 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -30,6 +30,8 @@ from plane.db.models import ( CommentReaction, IssueVote, IssueRelation, + State, + Project, ) @@ -69,19 +71,26 @@ class IssueProjectLiteSerializer(BaseSerializer): ##TODO: Find a better way to write this serializer ## Find a better approach to save manytomany? class IssueCreateSerializer(BaseSerializer): - state_detail = StateSerializer(read_only=True, source="state") - created_by_detail = UserLiteSerializer(read_only=True, source="created_by") - project_detail = ProjectLiteSerializer(read_only=True, source="project") - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") - - assignees = serializers.ListField( - child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), + # ids + state_id = serializers.PrimaryKeyRelatedField( + source="state", + queryset=State.objects.all(), + required=False, + allow_null=True, + ) + parent_id = serializers.PrimaryKeyRelatedField( + source="parent", + queryset=Issue.objects.all(), + required=False, + allow_null=True, + ) + label_ids = serializers.ListField( + child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), write_only=True, required=False, ) - - labels = serializers.ListField( - child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), + assignee_ids = serializers.ListField( + child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), write_only=True, required=False, ) @@ -100,8 +109,10 @@ class IssueCreateSerializer(BaseSerializer): def to_representation(self, instance): data = super().to_representation(instance) - data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()] - data['labels'] = [str(label.id) for label in instance.labels.all()] + assignee_ids = self.initial_data.get("assignee_ids") + data["assignee_ids"] = assignee_ids if assignee_ids else [] + label_ids = self.initial_data.get("label_ids") + data["label_ids"] = label_ids if label_ids else [] return data def validate(self, data): @@ -110,12 +121,14 @@ class IssueCreateSerializer(BaseSerializer): and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None) ): - raise serializers.ValidationError("Start date cannot exceed target date") + raise serializers.ValidationError( + "Start date cannot exceed target date" + ) return data def create(self, validated_data): - assignees = validated_data.pop("assignees", None) - labels = validated_data.pop("labels", None) + assignees = validated_data.pop("assignee_ids", None) + labels = validated_data.pop("label_ids", None) project_id = self.context["project_id"] workspace_id = self.context["workspace_id"] @@ -173,8 +186,8 @@ class IssueCreateSerializer(BaseSerializer): return issue def update(self, instance, validated_data): - assignees = validated_data.pop("assignees", None) - labels = validated_data.pop("labels", None) + assignees = validated_data.pop("assignee_ids", None) + labels = validated_data.pop("label_ids", None) # Related models project_id = instance.project_id @@ -225,14 +238,15 @@ 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") - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + workspace_detail = WorkspaceLiteSerializer( + read_only=True, source="workspace" + ) class Meta: model = IssueActivity fields = "__all__" - class IssuePropertySerializer(BaseSerializer): class Meta: model = IssueProperty @@ -245,7 +259,9 @@ class IssuePropertySerializer(BaseSerializer): class LabelSerializer(BaseSerializer): - workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + workspace_detail = WorkspaceLiteSerializer( + source="workspace", read_only=True + ) project_detail = ProjectLiteSerializer(source="project", read_only=True) class Meta: @@ -268,7 +284,6 @@ class LabelLiteSerializer(BaseSerializer): class IssueLabelSerializer(BaseSerializer): - class Meta: model = IssueLabel fields = "__all__" @@ -278,14 +293,19 @@ class IssueLabelSerializer(BaseSerializer): ] -class IssueRelationLiteSerializer(DynamicBaseSerializer): - project_id = serializers.PrimaryKeyRelatedField(read_only=True) +class IssueRelationSerializer(BaseSerializer): + id = serializers.UUIDField(source="related_issue.id", read_only=True) + project_id = serializers.PrimaryKeyRelatedField(source="related_issue.project_id", read_only=True) + sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True) + relation_type = serializers.CharField(read_only=True) + class Meta: - model = Issue + model = IssueRelation fields = [ "id", "project_id", "sequence_id", + "relation_type", ] read_only_fields = [ "workspace", @@ -293,26 +313,19 @@ class IssueRelationLiteSerializer(DynamicBaseSerializer): ] -class IssueRelationSerializer(BaseSerializer): - issue_detail = IssueRelationLiteSerializer(read_only=True, source="related_issue") - - class Meta: - model = IssueRelation - fields = [ - "issue_detail", - ] - read_only_fields = [ - "workspace", - "project", - ] - class RelatedIssueSerializer(BaseSerializer): - issue_detail = IssueRelationLiteSerializer(read_only=True, source="issue") + id = serializers.UUIDField(source="issue.id", read_only=True) + project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True) + sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True) + relation_type = serializers.CharField(read_only=True) class Meta: model = IssueRelation fields = [ - "issue_detail", + "id", + "project_id", + "sequence_id", + "relation_type", ] read_only_fields = [ "workspace", @@ -407,7 +420,8 @@ class IssueLinkSerializer(BaseSerializer): # Validation if url already exists def create(self, validated_data): if IssueLink.objects.filter( - url=validated_data.get("url"), issue_id=validated_data.get("issue_id") + url=validated_data.get("url"), + issue_id=validated_data.get("issue_id"), ).exists(): raise serializers.ValidationError( {"error": "URL already exists for this Issue"} @@ -431,9 +445,8 @@ class IssueAttachmentSerializer(BaseSerializer): class IssueReactionSerializer(BaseSerializer): - actor_detail = UserLiteSerializer(read_only=True, source="actor") - + class Meta: model = IssueReaction fields = "__all__" @@ -466,12 +479,18 @@ class CommentReactionSerializer(BaseSerializer): class IssueVoteSerializer(BaseSerializer): - actor_detail = UserLiteSerializer(read_only=True, source="actor") class Meta: model = IssueVote - fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"] + fields = [ + "issue", + "vote", + "workspace", + "project", + "actor", + "actor_detail", + ] read_only_fields = fields @@ -479,8 +498,12 @@ class IssueCommentSerializer(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") - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") - comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True) + workspace_detail = WorkspaceLiteSerializer( + read_only=True, source="workspace" + ) + comment_reactions = CommentReactionLiteSerializer( + read_only=True, many=True + ) is_member = serializers.BooleanField(read_only=True) class Meta: @@ -514,10 +537,14 @@ class IssueStateFlatSerializer(BaseSerializer): # Issue Serializer with state details class IssueStateSerializer(DynamicBaseSerializer): - label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) + label_details = LabelLiteSerializer( + read_only=True, source="labels", many=True + ) state_detail = StateLiteSerializer(read_only=True, source="state") project_detail = ProjectLiteSerializer(read_only=True, source="project") - assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) + assignee_details = UserLiteSerializer( + read_only=True, source="assignees", many=True + ) sub_issues_count = serializers.IntegerField(read_only=True) attachment_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True) @@ -536,15 +563,19 @@ class IssueSerializer(DynamicBaseSerializer): module_id = serializers.PrimaryKeyRelatedField(read_only=True) # Many to many - label_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="labels") - assignee_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="assignees") + label_ids = serializers.PrimaryKeyRelatedField( + read_only=True, many=True, source="labels" + ) + assignee_ids = serializers.PrimaryKeyRelatedField( + read_only=True, many=True, source="assignees" + ) # Count items sub_issues_count = serializers.IntegerField(read_only=True) attachment_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True) - # is + # is_subscribed is_subscribed = serializers.BooleanField(read_only=True) class Meta: @@ -582,11 +613,17 @@ class IssueSerializer(DynamicBaseSerializer): class IssueLiteSerializer(DynamicBaseSerializer): - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + workspace_detail = WorkspaceLiteSerializer( + read_only=True, source="workspace" + ) project_detail = ProjectLiteSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state") - label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) - assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) + label_details = LabelLiteSerializer( + read_only=True, source="labels", many=True + ) + assignee_details = UserLiteSerializer( + read_only=True, source="assignees", many=True + ) sub_issues_count = serializers.IntegerField(read_only=True) cycle_id = serializers.UUIDField(read_only=True) module_id = serializers.UUIDField(read_only=True) @@ -613,7 +650,9 @@ class IssueLiteSerializer(DynamicBaseSerializer): class IssuePublicSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state") - reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions") + reactions = IssueReactionSerializer( + read_only=True, many=True, source="issue_reactions" + ) votes = IssueVoteSerializer(read_only=True, many=True) class Meta: @@ -636,7 +675,6 @@ class IssuePublicSerializer(BaseSerializer): read_only_fields = fields - class IssueSubscriberSerializer(BaseSerializer): class Meta: model = IssueSubscriber diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index b38d05b2c..e94195671 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -26,7 +26,9 @@ class ModuleWriteSerializer(BaseSerializer): ) project_detail = ProjectLiteSerializer(source="project", read_only=True) - workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + workspace_detail = WorkspaceLiteSerializer( + source="workspace", read_only=True + ) class Meta: model = Module @@ -39,16 +41,22 @@ class ModuleWriteSerializer(BaseSerializer): "created_at", "updated_at", ] - + def to_representation(self, instance): data = super().to_representation(instance) - data['members'] = [str(member.id) for member in instance.members.all()] + data["members"] = [str(member.id) for member in instance.members.all()] return data def validate(self, data): - if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None): - raise serializers.ValidationError("Start date cannot exceed target date") - return data + if ( + data.get("start_date", None) is not None + and data.get("target_date", None) is not None + and data.get("start_date", None) > data.get("target_date", None) + ): + raise serializers.ValidationError( + "Start date cannot exceed target date" + ) + return data def create(self, validated_data): members = validated_data.pop("members", None) @@ -152,7 +160,8 @@ class ModuleLinkSerializer(BaseSerializer): # Validation if url already exists def create(self, validated_data): if ModuleLink.objects.filter( - url=validated_data.get("url"), module_id=validated_data.get("module_id") + url=validated_data.get("url"), + module_id=validated_data.get("module_id"), ).exists(): raise serializers.ValidationError( {"error": "URL already exists for this Issue"} @@ -163,7 +172,9 @@ class ModuleLinkSerializer(BaseSerializer): class ModuleSerializer(DynamicBaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") lead_detail = UserLiteSerializer(read_only=True, source="lead") - members_detail = UserLiteSerializer(read_only=True, many=True, source="members") + members_detail = UserLiteSerializer( + read_only=True, many=True, source="members" + ) link_module = ModuleLinkSerializer(read_only=True, many=True) is_favorite = serializers.BooleanField(read_only=True) total_issues = serializers.IntegerField(read_only=True) @@ -198,13 +209,9 @@ class ModuleFavoriteSerializer(BaseSerializer): "user", ] + class ModuleUserPropertiesSerializer(BaseSerializer): class Meta: model = ModuleUserProperties fields = "__all__" - read_only_fields = [ - "workspace", - "project", - "module", - "user" - ] \ No newline at end of file + read_only_fields = ["workspace", "project", "module", "user"] diff --git a/apiserver/plane/app/serializers/notification.py b/apiserver/plane/app/serializers/notification.py index b6a4f3e4a..70d876241 100644 --- a/apiserver/plane/app/serializers/notification.py +++ b/apiserver/plane/app/serializers/notification.py @@ -3,10 +3,12 @@ from .base import BaseSerializer from .user import UserLiteSerializer from plane.db.models import Notification + class NotificationSerializer(BaseSerializer): - triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by") + triggered_by_details = UserLiteSerializer( + read_only=True, source="triggered_by" + ) class Meta: model = Notification fields = "__all__" - diff --git a/apiserver/plane/app/serializers/page.py b/apiserver/plane/app/serializers/page.py index ff152627a..a0f5986d6 100644 --- a/apiserver/plane/app/serializers/page.py +++ b/apiserver/plane/app/serializers/page.py @@ -6,19 +6,31 @@ from .base import BaseSerializer from .issue import IssueFlatSerializer, LabelLiteSerializer from .workspace import WorkspaceLiteSerializer from .project import ProjectLiteSerializer -from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module +from plane.db.models import ( + Page, + PageLog, + PageFavorite, + PageLabel, + Label, + Issue, + Module, +) class PageSerializer(BaseSerializer): is_favorite = serializers.BooleanField(read_only=True) - label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) + label_details = LabelLiteSerializer( + read_only=True, source="labels", many=True + ) labels = serializers.ListField( child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), write_only=True, required=False, ) project_detail = ProjectLiteSerializer(source="project", read_only=True) - workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + workspace_detail = WorkspaceLiteSerializer( + source="workspace", read_only=True + ) class Meta: model = Page @@ -28,9 +40,10 @@ class PageSerializer(BaseSerializer): "project", "owned_by", ] + def to_representation(self, instance): data = super().to_representation(instance) - data['labels'] = [str(label.id) for label in instance.labels.all()] + data["labels"] = [str(label.id) for label in instance.labels.all()] return data def create(self, validated_data): @@ -94,7 +107,7 @@ class SubPageSerializer(BaseSerializer): def get_entity_details(self, obj): entity_name = obj.entity_name - if entity_name == 'forward_link' or entity_name == 'back_link': + if entity_name == "forward_link" or entity_name == "back_link": try: page = Page.objects.get(pk=obj.entity_identifier) return PageSerializer(page).data @@ -104,7 +117,6 @@ class SubPageSerializer(BaseSerializer): class PageLogSerializer(BaseSerializer): - class Meta: model = PageLog fields = "__all__" diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index b3122962b..999233442 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -4,7 +4,10 @@ from rest_framework import serializers # Module imports from .base import BaseSerializer, DynamicBaseSerializer from plane.app.serializers.workspace import WorkspaceLiteSerializer -from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer +from plane.app.serializers.user import ( + UserLiteSerializer, + UserAdminLiteSerializer, +) from plane.db.models import ( Project, ProjectMember, @@ -17,7 +20,9 @@ from plane.db.models import ( class ProjectSerializer(BaseSerializer): - workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + workspace_detail = WorkspaceLiteSerializer( + source="workspace", read_only=True + ) class Meta: model = Project @@ -29,12 +34,16 @@ class ProjectSerializer(BaseSerializer): def create(self, validated_data): identifier = validated_data.get("identifier", "").strip().upper() if identifier == "": - raise serializers.ValidationError(detail="Project Identifier is required") + raise serializers.ValidationError( + detail="Project Identifier is required" + ) if ProjectIdentifier.objects.filter( name=identifier, workspace_id=self.context["workspace_id"] ).exists(): - raise serializers.ValidationError(detail="Project Identifier is taken") + raise serializers.ValidationError( + detail="Project Identifier is taken" + ) project = Project.objects.create( **validated_data, workspace_id=self.context["workspace_id"] ) @@ -73,7 +82,9 @@ class ProjectSerializer(BaseSerializer): return project # If not same fail update - raise serializers.ValidationError(detail="Project Identifier is already taken") + raise serializers.ValidationError( + detail="Project Identifier is already taken" + ) class ProjectLiteSerializer(BaseSerializer): @@ -159,12 +170,13 @@ class ProjectMemberAdminSerializer(BaseSerializer): model = ProjectMember fields = "__all__" -class ProjectMemberRoleSerializer(DynamicBaseSerializer): +class ProjectMemberRoleSerializer(DynamicBaseSerializer): class Meta: model = ProjectMember fields = ("id", "role", "member", "project") + class ProjectMemberInviteSerializer(BaseSerializer): project = ProjectLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) @@ -202,7 +214,9 @@ class ProjectMemberLiteSerializer(BaseSerializer): class ProjectDeployBoardSerializer(BaseSerializer): project_details = ProjectLiteSerializer(read_only=True, source="project") - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + workspace_detail = WorkspaceLiteSerializer( + read_only=True, source="workspace" + ) class Meta: model = ProjectDeployBoard @@ -222,4 +236,4 @@ class ProjectPublicMemberSerializer(BaseSerializer): "workspace", "project", "member", - ] \ No newline at end of file + ] diff --git a/apiserver/plane/app/serializers/state.py b/apiserver/plane/app/serializers/state.py index 323254f26..25dded62d 100644 --- a/apiserver/plane/app/serializers/state.py +++ b/apiserver/plane/app/serializers/state.py @@ -6,7 +6,6 @@ from plane.db.models import State class StateSerializer(BaseSerializer): - class Meta: model = State fields = "__all__" @@ -25,4 +24,4 @@ class StateLiteSerializer(BaseSerializer): "color", "group", ] - read_only_fields = fields \ No newline at end of file + read_only_fields = fields diff --git a/apiserver/plane/app/serializers/user.py b/apiserver/plane/app/serializers/user.py index 1b94758e8..8cd48827e 100644 --- a/apiserver/plane/app/serializers/user.py +++ b/apiserver/plane/app/serializers/user.py @@ -99,7 +99,9 @@ class UserMeSettingsSerializer(BaseSerializer): ).first() return { "last_workspace_id": obj.last_workspace_id, - "last_workspace_slug": workspace.slug if workspace is not None else "", + "last_workspace_slug": workspace.slug + if workspace is not None + else "", "fallback_workspace_id": obj.last_workspace_id, "fallback_workspace_slug": workspace.slug if workspace is not None @@ -109,7 +111,8 @@ class UserMeSettingsSerializer(BaseSerializer): else: fallback_workspace = ( Workspace.objects.filter( - workspace_member__member_id=obj.id, workspace_member__is_active=True + workspace_member__member_id=obj.id, + workspace_member__is_active=True, ) .order_by("created_at") .first() @@ -180,7 +183,9 @@ class ChangePasswordSerializer(serializers.Serializer): if data.get("new_password") != data.get("confirm_password"): raise serializers.ValidationError( - {"error": "Confirm password should be same as the new password."} + { + "error": "Confirm password should be same as the new password." + } ) return data @@ -190,4 +195,5 @@ class ResetPasswordSerializer(serializers.Serializer): """ Serializer for password change endpoint. """ + new_password = serializers.CharField(required=True, min_length=8) diff --git a/apiserver/plane/app/serializers/view.py b/apiserver/plane/app/serializers/view.py index db44a2fc0..f864f2b6c 100644 --- a/apiserver/plane/app/serializers/view.py +++ b/apiserver/plane/app/serializers/view.py @@ -10,7 +10,9 @@ from plane.utils.issue_filters import issue_filters class GlobalViewSerializer(BaseSerializer): - workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + workspace_detail = WorkspaceLiteSerializer( + source="workspace", read_only=True + ) class Meta: model = GlobalView @@ -41,7 +43,9 @@ class GlobalViewSerializer(BaseSerializer): class IssueViewSerializer(DynamicBaseSerializer): is_favorite = serializers.BooleanField(read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True) - workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + workspace_detail = WorkspaceLiteSerializer( + source="workspace", read_only=True + ) class Meta: model = IssueView @@ -80,4 +84,4 @@ class IssueViewFavoriteSerializer(BaseSerializer): "workspace", "project", "user", - ] \ No newline at end of file + ] diff --git a/apiserver/plane/app/serializers/webhook.py b/apiserver/plane/app/serializers/webhook.py index 961466d28..95ca149ff 100644 --- a/apiserver/plane/app/serializers/webhook.py +++ b/apiserver/plane/app/serializers/webhook.py @@ -10,78 +10,113 @@ 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 +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."}) + 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."}) + raise serializers.ValidationError( + {"url": "Hostname could not be resolved."} + ) if not ip_addresses: - raise serializers.ValidationError({"url": "No IP addresses found for the hostname."}) + 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_private or ip.is_loopback: - raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."}) + 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 + 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 + 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."}) + 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."}) + 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."}) + raise serializers.ValidationError( + {"url": "Hostname could not be resolved."} + ) if not ip_addresses: - raise serializers.ValidationError({"url": "No IP addresses found for the hostname."}) + 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_private or ip.is_loopback: - raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."}) + 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 + 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 + 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."}) + 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) @@ -95,12 +130,7 @@ class WebhookSerializer(DynamicBaseSerializer): class WebhookLogSerializer(DynamicBaseSerializer): - class Meta: model = WebhookLog fields = "__all__" - read_only_fields = [ - "workspace", - "webhook" - ] - + read_only_fields = ["workspace", "webhook"] diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index fe014f364..69f827c24 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -51,6 +51,7 @@ class WorkSpaceSerializer(DynamicBaseSerializer): "owner", ] + class WorkspaceLiteSerializer(BaseSerializer): class Meta: model = Workspace @@ -62,7 +63,6 @@ class WorkspaceLiteSerializer(BaseSerializer): read_only_fields = fields - class WorkSpaceMemberSerializer(DynamicBaseSerializer): member = UserLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) @@ -73,7 +73,6 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer): class WorkspaceMemberMeSerializer(BaseSerializer): - class Meta: model = WorkspaceMember fields = "__all__" @@ -109,7 +108,9 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer): class TeamSerializer(BaseSerializer): - members_detail = UserLiteSerializer(read_only=True, source="members", many=True) + members_detail = UserLiteSerializer( + read_only=True, source="members", many=True + ) members = serializers.ListField( child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), write_only=True, @@ -146,7 +147,9 @@ class TeamSerializer(BaseSerializer): members = validated_data.pop("members") TeamMember.objects.filter(team=instance).delete() team_members = [ - TeamMember(member=member, team=instance, workspace=instance.workspace) + TeamMember( + member=member, team=instance, workspace=instance.workspace + ) for member in members ] TeamMember.objects.bulk_create(team_members, batch_size=10) @@ -171,4 +174,4 @@ class WorkspaceUserPropertiesSerializer(BaseSerializer): read_only_fields = [ "workspace", "user", - ] \ No newline at end of file + ] diff --git a/apiserver/plane/app/urls/__init__.py b/apiserver/plane/app/urls/__init__.py index d8334ed57..f2b11f127 100644 --- a/apiserver/plane/app/urls/__init__.py +++ b/apiserver/plane/app/urls/__init__.py @@ -3,6 +3,7 @@ from .asset import urlpatterns as asset_urls from .authentication import urlpatterns as authentication_urls from .config import urlpatterns as configuration_urls from .cycle import urlpatterns as cycle_urls +from .dashboard import urlpatterns as dashboard_urls from .estimate import urlpatterns as estimate_urls from .external import urlpatterns as external_urls from .importer import urlpatterns as importer_urls @@ -28,6 +29,7 @@ urlpatterns = [ *authentication_urls, *configuration_urls, *cycle_urls, + *dashboard_urls, *estimate_urls, *external_urls, *importer_urls, @@ -45,4 +47,4 @@ urlpatterns = [ *workspace_urls, *api_urls, *webhook_urls, -] \ No newline at end of file +] diff --git a/apiserver/plane/app/urls/authentication.py b/apiserver/plane/app/urls/authentication.py index 39986f791..e91e5706b 100644 --- a/apiserver/plane/app/urls/authentication.py +++ b/apiserver/plane/app/urls/authentication.py @@ -31,8 +31,14 @@ urlpatterns = [ path("sign-in/", SignInEndpoint.as_view(), name="sign-in"), path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), # magic sign in - path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"), - path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"), + path( + "magic-generate/", + MagicGenerateEndpoint.as_view(), + name="magic-generate", + ), + path( + "magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in" + ), path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), # Password Manipulation path( @@ -52,6 +58,8 @@ urlpatterns = [ ), # API Tokens path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), - path("api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens"), + path( + "api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens" + ), ## End API Tokens ] diff --git a/apiserver/plane/app/urls/config.py b/apiserver/plane/app/urls/config.py index 12beb63aa..3ea825eb2 100644 --- a/apiserver/plane/app/urls/config.py +++ b/apiserver/plane/app/urls/config.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.app.views import ConfigurationEndpoint +from plane.app.views import ConfigurationEndpoint, MobileConfigurationEndpoint urlpatterns = [ path( @@ -9,4 +9,9 @@ urlpatterns = [ ConfigurationEndpoint.as_view(), name="configuration", ), -] \ No newline at end of file + path( + "mobile-configs/", + MobileConfigurationEndpoint.as_view(), + name="configuration", + ), +] diff --git a/apiserver/plane/app/urls/cycle.py b/apiserver/plane/app/urls/cycle.py index 5fef437c6..0d57e77f7 100644 --- a/apiserver/plane/app/urls/cycle.py +++ b/apiserver/plane/app/urls/cycle.py @@ -8,10 +8,16 @@ from plane.app.views import ( CycleFavoriteViewSet, TransferCycleIssueEndpoint, CycleUserPropertiesEndpoint, + ActiveCycleEndpoint ) urlpatterns = [ + path( + "workspaces//active-cycles/", + ActiveCycleEndpoint.as_view(), + name="workspace-active-cycle", + ), path( "workspaces//projects//cycles/", CycleViewSet.as_view( @@ -89,5 +95,5 @@ urlpatterns = [ "workspaces//projects//cycles//user-properties/", CycleUserPropertiesEndpoint.as_view(), name="cycle-user-filters", - ) + ), ] diff --git a/apiserver/plane/app/urls/dashboard.py b/apiserver/plane/app/urls/dashboard.py new file mode 100644 index 000000000..0dc24a808 --- /dev/null +++ b/apiserver/plane/app/urls/dashboard.py @@ -0,0 +1,23 @@ +from django.urls import path + + +from plane.app.views import DashboardEndpoint, WidgetsEndpoint + + +urlpatterns = [ + path( + "workspaces//dashboard/", + DashboardEndpoint.as_view(), + name="dashboard", + ), + path( + "workspaces//dashboard//", + DashboardEndpoint.as_view(), + name="dashboard", + ), + path( + "dashboard//widgets//", + WidgetsEndpoint.as_view(), + name="widgets", + ), +] diff --git a/apiserver/plane/app/urls/module.py b/apiserver/plane/app/urls/module.py index 961fff0db..d81d32d3a 100644 --- a/apiserver/plane/app/urls/module.py +++ b/apiserver/plane/app/urls/module.py @@ -7,7 +7,7 @@ from plane.app.views import ( ModuleLinkViewSet, ModuleFavoriteViewSet, BulkImportModulesEndpoint, - ModuleUserPropertiesEndpoint + ModuleUserPropertiesEndpoint, ) @@ -106,5 +106,5 @@ urlpatterns = [ "workspaces//projects//modules//user-properties/", ModuleUserPropertiesEndpoint.as_view(), name="cycle-user-filters", - ) + ), ] diff --git a/apiserver/plane/app/urls/project.py b/apiserver/plane/app/urls/project.py index 39456a830..f8ecac4c0 100644 --- a/apiserver/plane/app/urls/project.py +++ b/apiserver/plane/app/urls/project.py @@ -175,4 +175,4 @@ urlpatterns = [ ), name="project-deploy-board", ), -] \ No newline at end of file +] diff --git a/apiserver/plane/app/urls/views.py b/apiserver/plane/app/urls/views.py index f78f17869..36372c03a 100644 --- a/apiserver/plane/app/urls/views.py +++ b/apiserver/plane/app/urls/views.py @@ -5,7 +5,7 @@ from plane.app.views import ( IssueViewViewSet, GlobalViewViewSet, GlobalViewIssuesViewSet, - IssueViewFavoriteViewSet, + IssueViewFavoriteViewSet, ) diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index cc78881f9..d2d8f5c06 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -206,5 +206,5 @@ urlpatterns = [ "workspaces//user-properties/", WorkspaceUserPropertiesEndpoint.as_view(), name="workspace-user-filters", - ) + ), ] diff --git a/apiserver/plane/app/urls_deprecated.py b/apiserver/plane/app/urls_deprecated.py index c6e6183fa..2a47285aa 100644 --- a/apiserver/plane/app/urls_deprecated.py +++ b/apiserver/plane/app/urls_deprecated.py @@ -192,7 +192,7 @@ from plane.app.views import ( ) -#TODO: Delete this file +# TODO: Delete this file # This url file has been deprecated use apiserver/plane/urls folder to create new urls urlpatterns = [ @@ -204,10 +204,14 @@ urlpatterns = [ path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), # Magic Sign In/Up path( - "magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate" + "magic-generate/", + MagicSignInGenerateEndpoint.as_view(), + name="magic-generate", ), - path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"), - path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path( + "magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in" + ), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), # Email verification path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"), path( @@ -272,7 +276,9 @@ urlpatterns = [ # user workspace invitations path( "users/me/invitations/workspaces/", - UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}), + UserWorkspaceInvitationsEndpoint.as_view( + {"get": "list", "post": "create"} + ), name="user-workspace-invitations", ), # user workspace invitation @@ -311,7 +317,9 @@ urlpatterns = [ # user project invitations path( "users/me/invitations/projects/", - UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}), + UserProjectInvitationsViewset.as_view( + {"get": "list", "post": "create"} + ), name="user-project-invitaions", ), ## Workspaces ## @@ -1238,7 +1246,7 @@ urlpatterns = [ "post": "unarchive", } ), - name="project-page-unarchive" + name="project-page-unarchive", ), path( "workspaces//projects//archived-pages/", @@ -1264,19 +1272,22 @@ urlpatterns = [ { "post": "unlock", } - ) + ), ), path( "workspaces//projects//pages//transactions/", - PageLogEndpoint.as_view(), name="page-transactions" + PageLogEndpoint.as_view(), + name="page-transactions", ), path( "workspaces//projects//pages//transactions//", - PageLogEndpoint.as_view(), name="page-transactions" + PageLogEndpoint.as_view(), + name="page-transactions", ), path( "workspaces//projects//pages//sub-pages/", - SubPagesEndpoint.as_view(), name="sub-page" + SubPagesEndpoint.as_view(), + name="sub-page", ), path( "workspaces//projects//estimates/", @@ -1326,7 +1337,9 @@ urlpatterns = [ ## End Pages # API Tokens path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), - path("api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens"), + path( + "api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens" + ), ## End API Tokens # Integrations path( diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 520a3fd38..24684b7fa 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -62,6 +62,7 @@ from .cycle import ( CycleFavoriteViewSet, TransferCycleIssueEndpoint, CycleUserPropertiesEndpoint, + ActiveCycleEndpoint, ) from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet from .issue import ( @@ -140,7 +141,11 @@ from .page import ( from .search import GlobalSearchEndpoint, IssueSearchEndpoint -from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint +from .external import ( + GPTIntegrationEndpoint, + ReleaseNotesEndpoint, + UnsplashEndpoint, +) from .estimate import ( ProjectEstimatePointEndpoint, @@ -165,10 +170,15 @@ from .notification import ( from .exporter import ExportIssuesEndpoint -from .config import ConfigurationEndpoint +from .config import ConfigurationEndpoint, MobileConfigurationEndpoint from .webhook import ( WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint, ) + +from .dashboard import ( + DashboardEndpoint, + WidgetsEndpoint +) \ No newline at end of file diff --git a/apiserver/plane/app/views/analytic.py b/apiserver/plane/app/views/analytic.py index c1deb0d8f..04a77f789 100644 --- a/apiserver/plane/app/views/analytic.py +++ b/apiserver/plane/app/views/analytic.py @@ -61,7 +61,9 @@ class AnalyticsEndpoint(BaseAPIView): ) # If segment is present it cannot be same as x-axis - if segment and (segment not in valid_xaxis_segment or x_axis == segment): + if segment and ( + segment not in valid_xaxis_segment or x_axis == segment + ): return Response( { "error": "Both segment and x axis cannot be same and segment should be valid" @@ -110,7 +112,9 @@ class AnalyticsEndpoint(BaseAPIView): if x_axis in ["assignees__id"] or segment in ["assignees__id"]: assignee_details = ( Issue.issue_objects.filter( - workspace__slug=slug, **filters, assignees__avatar__isnull=False + workspace__slug=slug, + **filters, + assignees__avatar__isnull=False, ) .order_by("assignees__id") .distinct("assignees__id") @@ -124,7 +128,9 @@ class AnalyticsEndpoint(BaseAPIView): ) cycle_details = {} - if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]: + if x_axis in ["issue_cycle__cycle_id"] or segment in [ + "issue_cycle__cycle_id" + ]: cycle_details = ( Issue.issue_objects.filter( workspace__slug=slug, @@ -186,7 +192,9 @@ class AnalyticViewViewset(BaseViewSet): def get_queryset(self): return self.filter_queryset( - super().get_queryset().filter(workspace__slug=self.kwargs.get("slug")) + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) ) @@ -196,7 +204,9 @@ class SavedAnalyticEndpoint(BaseAPIView): ] def get(self, request, slug, analytic_id): - analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug) + analytic_view = AnalyticView.objects.get( + pk=analytic_id, workspace__slug=slug + ) filter = analytic_view.query queryset = Issue.issue_objects.filter(**filter) @@ -266,7 +276,9 @@ class ExportAnalyticsEndpoint(BaseAPIView): ) # If segment is present it cannot be same as x-axis - if segment and (segment not in valid_xaxis_segment or x_axis == segment): + if segment and ( + segment not in valid_xaxis_segment or x_axis == segment + ): return Response( { "error": "Both segment and x axis cannot be same and segment should be valid" @@ -293,7 +305,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView): def get(self, request, slug): filters = issue_filters(request.GET, "GET") - base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters) + base_issues = Issue.issue_objects.filter( + workspace__slug=slug, **filters + ) total_issues = base_issues.count() @@ -306,7 +320,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView): ) open_issues_groups = ["backlog", "unstarted", "started"] - open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups) + open_issues_queryset = state_groups.filter( + state__group__in=open_issues_groups + ) open_issues = open_issues_queryset.count() open_issues_classified = ( @@ -361,10 +377,12 @@ class DefaultAnalyticsEndpoint(BaseAPIView): .order_by("-count") ) - open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("estimate_point"))[ + open_estimate_sum = open_issues_queryset.aggregate( + sum=Sum("estimate_point") + )["sum"] + total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))[ "sum" ] - total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))["sum"] return Response( { diff --git a/apiserver/plane/app/views/api.py b/apiserver/plane/app/views/api.py index ce2d4bd09..86a29c7fa 100644 --- a/apiserver/plane/app/views/api.py +++ b/apiserver/plane/app/views/api.py @@ -71,7 +71,9 @@ class ApiTokenEndpoint(BaseAPIView): user=request.user, pk=pk, ) - serializer = APITokenSerializer(api_token, data=request.data, partial=True) + serializer = APITokenSerializer( + api_token, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/asset.py b/apiserver/plane/app/views/asset.py index 17d70d936..fb5590610 100644 --- a/apiserver/plane/app/views/asset.py +++ b/apiserver/plane/app/views/asset.py @@ -10,7 +10,11 @@ from plane.app.serializers import FileAssetSerializer class FileAssetEndpoint(BaseAPIView): - parser_classes = (MultiPartParser, FormParser, JSONParser,) + parser_classes = ( + MultiPartParser, + FormParser, + JSONParser, + ) """ A viewset for viewing and editing task instances. @@ -20,10 +24,18 @@ class FileAssetEndpoint(BaseAPIView): asset_key = str(workspace_id) + "/" + asset_key files = FileAsset.objects.filter(asset=asset_key) if files.exists(): - serializer = FileAssetSerializer(files, context={"request": request}, many=True) - return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) + serializer = FileAssetSerializer( + files, context={"request": request}, many=True + ) + return Response( + {"data": serializer.data, "status": True}, + status=status.HTTP_200_OK, + ) else: - return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) + return Response( + {"error": "Asset key does not exist", "status": False}, + status=status.HTTP_200_OK, + ) def post(self, request, slug): serializer = FileAssetSerializer(data=request.data) @@ -33,7 +45,7 @@ class FileAssetEndpoint(BaseAPIView): 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) - + def delete(self, request, workspace_id, asset_key): asset_key = str(workspace_id) + "/" + asset_key file_asset = FileAsset.objects.get(asset=asset_key) @@ -43,7 +55,6 @@ class FileAssetEndpoint(BaseAPIView): class FileAssetViewSet(BaseViewSet): - def restore(self, request, workspace_id, asset_key): asset_key = str(workspace_id) + "/" + asset_key file_asset = FileAsset.objects.get(asset=asset_key) @@ -56,12 +67,22 @@ class UserAssetsEndpoint(BaseAPIView): parser_classes = (MultiPartParser, FormParser) def get(self, request, asset_key): - files = FileAsset.objects.filter(asset=asset_key, created_by=request.user) + files = FileAsset.objects.filter( + asset=asset_key, created_by=request.user + ) if files.exists(): - serializer = FileAssetSerializer(files, context={"request": request}) - return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) + serializer = FileAssetSerializer( + files, context={"request": request} + ) + return Response( + {"data": serializer.data, "status": True}, + status=status.HTTP_200_OK, + ) else: - return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) + return Response( + {"error": "Asset key does not exist", "status": False}, + status=status.HTTP_200_OK, + ) def post(self, request): serializer = FileAssetSerializer(data=request.data) @@ -70,9 +91,10 @@ class UserAssetsEndpoint(BaseAPIView): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def delete(self, request, asset_key): - file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user) + file_asset = FileAsset.objects.get( + asset=asset_key, created_by=request.user + ) file_asset.is_deleted = True file_asset.save() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/auth_extended.py b/apiserver/plane/app/views/auth_extended.py index 049e5aab9..501f47657 100644 --- a/apiserver/plane/app/views/auth_extended.py +++ b/apiserver/plane/app/views/auth_extended.py @@ -128,7 +128,8 @@ class ForgotPasswordEndpoint(BaseAPIView): status=status.HTTP_200_OK, ) return Response( - {"error": "Please check the email"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Please check the email"}, + status=status.HTTP_400_BAD_REQUEST, ) @@ -167,7 +168,9 @@ class ResetPasswordEndpoint(BaseAPIView): } return Response(data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) except DjangoUnicodeDecodeError as indentifier: return Response( @@ -191,7 +194,8 @@ class ChangePasswordEndpoint(BaseAPIView): user.is_password_autoset = False user.save() return Response( - {"message": "Password updated successfully"}, status=status.HTTP_200_OK + {"message": "Password updated successfully"}, + status=status.HTTP_200_OK, ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -213,7 +217,8 @@ class SetUserPasswordEndpoint(BaseAPIView): # Check password validation if not password and len(str(password)) < 8: return Response( - {"error": "Password is not valid"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Password is not valid"}, + status=status.HTTP_400_BAD_REQUEST, ) # Set the user password @@ -281,7 +286,9 @@ class MagicGenerateEndpoint(BaseAPIView): if data["current_attempt"] > 2: return Response( - {"error": "Max attempts exhausted. Please try again later."}, + { + "error": "Max attempts exhausted. Please try again later." + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -339,7 +346,8 @@ class EmailCheckEndpoint(BaseAPIView): if not email: return Response( - {"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Email is required"}, + status=status.HTTP_400_BAD_REQUEST, ) # validate the email @@ -347,7 +355,8 @@ class EmailCheckEndpoint(BaseAPIView): validate_email(email) except ValidationError: return Response( - {"error": "Email is not valid"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Email is not valid"}, + status=status.HTTP_400_BAD_REQUEST, ) # Check if the user exists @@ -399,13 +408,18 @@ class EmailCheckEndpoint(BaseAPIView): key, token, current_attempt = generate_magic_token(email=email) if not current_attempt: return Response( - {"error": "Max attempts exhausted. Please try again later."}, + { + "error": "Max attempts exhausted. Please try again later." + }, status=status.HTTP_400_BAD_REQUEST, ) # Trigger the email magic_link.delay(email, "magic_" + str(email), token, current_site) return Response( - {"is_password_autoset": user.is_password_autoset, "is_existing": False}, + { + "is_password_autoset": user.is_password_autoset, + "is_existing": False, + }, status=status.HTTP_200_OK, ) @@ -433,7 +447,9 @@ class EmailCheckEndpoint(BaseAPIView): key, token, current_attempt = generate_magic_token(email=email) if not current_attempt: return Response( - {"error": "Max attempts exhausted. Please try again later."}, + { + "error": "Max attempts exhausted. Please try again later." + }, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/app/views/authentication.py b/apiserver/plane/app/views/authentication.py index 256446313..a41200d61 100644 --- a/apiserver/plane/app/views/authentication.py +++ b/apiserver/plane/app/views/authentication.py @@ -73,7 +73,7 @@ class SignUpEndpoint(BaseAPIView): # get configuration values # Get configuration values - ENABLE_SIGNUP, = get_configuration_value( + (ENABLE_SIGNUP,) = get_configuration_value( [ { "key": "ENABLE_SIGNUP", @@ -173,7 +173,7 @@ class SignInEndpoint(BaseAPIView): # Create the user else: - ENABLE_SIGNUP, = get_configuration_value( + (ENABLE_SIGNUP,) = get_configuration_value( [ { "key": "ENABLE_SIGNUP", @@ -325,7 +325,7 @@ class MagicSignInEndpoint(BaseAPIView): ) user_token = request.data.get("token", "").strip() - key = request.data.get("key", False).strip().lower() + key = request.data.get("key", "").strip().lower() if not key or user_token == "": return Response( @@ -364,8 +364,10 @@ class MagicSignInEndpoint(BaseAPIView): user.save() # Check if user has any accepted invites for workspace and add them to workspace - workspace_member_invites = WorkspaceMemberInvite.objects.filter( - email=user.email, accepted=True + workspace_member_invites = ( + WorkspaceMemberInvite.objects.filter( + email=user.email, accepted=True + ) ) WorkspaceMember.objects.bulk_create( @@ -431,7 +433,9 @@ class MagicSignInEndpoint(BaseAPIView): else: return Response( - {"error": "Your login code was incorrect. Please try again."}, + { + "error": "Your login code was incorrect. Please try again." + }, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index 5bd79cb96..e07cb811c 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -46,7 +46,9 @@ class WebhookMixin: bulk = False def finalize_response(self, request, response, *args, **kwargs): - response = super().finalize_response(request, response, *args, **kwargs) + response = super().finalize_response( + request, response, *args, **kwargs + ) # Check for the case should webhook be sent if ( @@ -88,7 +90,9 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): return self.model.objects.all() except Exception as e: capture_exception(e) - raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST) + raise APIException( + "Please check the view", status.HTTP_400_BAD_REQUEST + ) def handle_exception(self, exc): """ @@ -99,6 +103,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): response = super().handle_exception(exc) return response except Exception as e: + print(e) if settings.DEBUG else print("Server Error") if isinstance(e, IntegrityError): return Response( {"error": "The payload is not valid"}, @@ -112,23 +117,23 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): ) if isinstance(e, ObjectDoesNotExist): - model_name = str(exc).split(" matching query does not exist.")[0] return Response( - {"error": f"{model_name} does not exist."}, + {"error": f"The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) if isinstance(e, KeyError): capture_exception(e) return Response( - {"error": f"key {e} does not exist"}, + {"error": f"The required key does not exist."}, status=status.HTTP_400_BAD_REQUEST, ) - - print(e) if settings.DEBUG else print("Server Error") - capture_exception(e) - return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) def dispatch(self, request, *args, **kwargs): try: @@ -162,19 +167,22 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): @property def fields(self): fields = [ - field for field in self.request.GET.get("fields", "").split(",") if field + field + for field in self.request.GET.get("fields", "").split(",") + if field ] return fields if fields else None @property def expand(self): expand = [ - expand for expand in self.request.GET.get("expand", "").split(",") if expand + expand + for expand in self.request.GET.get("expand", "").split(",") + if expand ] return expand if expand else None - class BaseAPIView(TimezoneMixin, APIView, BasePaginator): permission_classes = [ IsAuthenticated, @@ -216,20 +224,24 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): ) if isinstance(e, ObjectDoesNotExist): - model_name = str(exc).split(" matching query does not exist.")[0] return Response( - {"error": f"{model_name} does not exist."}, + {"error": f"The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) - + if isinstance(e, KeyError): - return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": f"The required key does not exist."}, + status=status.HTTP_400_BAD_REQUEST, + ) if settings.DEBUG: print(e) capture_exception(e) - return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) def dispatch(self, request, *args, **kwargs): try: @@ -258,13 +270,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): @property def fields(self): fields = [ - field for field in self.request.GET.get("fields", "").split(",") if field + field + for field in self.request.GET.get("fields", "").split(",") + if field ] return fields if fields else None @property def expand(self): expand = [ - expand for expand in self.request.GET.get("expand", "").split(",") if expand + expand + for expand in self.request.GET.get("expand", "").split(",") + if expand ] return expand if expand else None diff --git a/apiserver/plane/app/views/config.py b/apiserver/plane/app/views/config.py index 80467c90d..29b4bbf8b 100644 --- a/apiserver/plane/app/views/config.py +++ b/apiserver/plane/app/views/config.py @@ -90,10 +90,14 @@ class ConfigurationEndpoint(BaseAPIView): data = {} # Authentication data["google_client_id"] = ( - GOOGLE_CLIENT_ID if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != '""' else None + GOOGLE_CLIENT_ID + if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != '""' + else None ) data["github_client_id"] = ( - GITHUB_CLIENT_ID if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != '""' else None + GITHUB_CLIENT_ID + if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != '""' + else None ) data["github_app_name"] = GITHUB_APP_NAME data["magic_login"] = ( @@ -115,7 +119,125 @@ class ConfigurationEndpoint(BaseAPIView): data["has_openai_configured"] = bool(OPENAI_API_KEY) # File size settings - data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880)) + data["file_size_limit"] = float( + os.environ.get("FILE_SIZE_LIMIT", 5242880) + ) + + # is smtp configured + data["is_smtp_configured"] = bool(EMAIL_HOST_USER) and bool( + EMAIL_HOST_PASSWORD + ) + + return Response(data, status=status.HTTP_200_OK) + + +class MobileConfigurationEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request): + ( + GOOGLE_CLIENT_ID, + GOOGLE_SERVER_CLIENT_ID, + GOOGLE_IOS_CLIENT_ID, + EMAIL_HOST_USER, + EMAIL_HOST_PASSWORD, + ENABLE_MAGIC_LINK_LOGIN, + ENABLE_EMAIL_PASSWORD, + POSTHOG_API_KEY, + POSTHOG_HOST, + UNSPLASH_ACCESS_KEY, + OPENAI_API_KEY, + ) = get_configuration_value( + [ + { + "key": "GOOGLE_CLIENT_ID", + "default": os.environ.get("GOOGLE_CLIENT_ID", None), + }, + { + "key": "GOOGLE_SERVER_CLIENT_ID", + "default": os.environ.get("GOOGLE_SERVER_CLIENT_ID", None), + }, + { + "key": "GOOGLE_IOS_CLIENT_ID", + "default": os.environ.get("GOOGLE_IOS_CLIENT_ID", None), + }, + { + "key": "EMAIL_HOST_USER", + "default": os.environ.get("EMAIL_HOST_USER", None), + }, + { + "key": "EMAIL_HOST_PASSWORD", + "default": os.environ.get("EMAIL_HOST_PASSWORD", None), + }, + { + "key": "ENABLE_MAGIC_LINK_LOGIN", + "default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"), + }, + { + "key": "ENABLE_EMAIL_PASSWORD", + "default": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"), + }, + { + "key": "POSTHOG_API_KEY", + "default": os.environ.get("POSTHOG_API_KEY", "1"), + }, + { + "key": "POSTHOG_HOST", + "default": os.environ.get("POSTHOG_HOST", "1"), + }, + { + "key": "UNSPLASH_ACCESS_KEY", + "default": os.environ.get("UNSPLASH_ACCESS_KEY", "1"), + }, + { + "key": "OPENAI_API_KEY", + "default": os.environ.get("OPENAI_API_KEY", "1"), + }, + ] + ) + data = {} + # Authentication + data["google_client_id"] = ( + GOOGLE_CLIENT_ID + if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != '""' + else None + ) + data["google_server_client_id"] = ( + GOOGLE_SERVER_CLIENT_ID + if GOOGLE_SERVER_CLIENT_ID and GOOGLE_SERVER_CLIENT_ID != '""' + else None + ) + data["google_ios_client_id"] = ( + (GOOGLE_IOS_CLIENT_ID)[::-1] + if GOOGLE_IOS_CLIENT_ID is not None + else None + ) + # Posthog + data["posthog_api_key"] = POSTHOG_API_KEY + data["posthog_host"] = POSTHOG_HOST + + data["magic_login"] = ( + bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD) + ) and ENABLE_MAGIC_LINK_LOGIN == "1" + + data["email_password_login"] = ENABLE_EMAIL_PASSWORD == "1" + + # Posthog + data["posthog_api_key"] = POSTHOG_API_KEY + data["posthog_host"] = POSTHOG_HOST + + # Unsplash + data["has_unsplash_configured"] = bool(UNSPLASH_ACCESS_KEY) + + # Open AI settings + data["has_openai_configured"] = bool(OPENAI_API_KEY) + + # File size settings + data["file_size_limit"] = float( + os.environ.get("FILE_SIZE_LIMIT", 5242880) + ) # is smtp configured data["is_smtp_configured"] = not ( diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index 73741b983..4db0ec565 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -31,11 +31,16 @@ from plane.app.serializers import ( CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, + IssueSerializer, IssueStateSerializer, CycleWriteSerializer, CycleUserPropertiesSerializer, ) -from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission +from plane.app.permissions import ( + ProjectEntityPermission, + ProjectLitePermission, + WorkspaceUserPermission +) from plane.db.models import ( User, Cycle, @@ -46,9 +51,9 @@ from plane.db.models import ( IssueAttachment, Label, CycleUserProperties, + IssueSubscriber, ) from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters from plane.utils.analytics_plot import burndown_plot @@ -63,7 +68,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet): def perform_create(self, serializer): serializer.save( - project_id=self.kwargs.get("project_id"), owned_by=self.request.user + project_id=self.kwargs.get("project_id"), + owned_by=self.request.user, ) def get_queryset(self): @@ -142,7 +148,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet): ), ) ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) + .annotate( + total_estimates=Sum("issue_cycle__issue__estimate_point") + ) .annotate( completed_estimates=Sum( "issue_cycle__issue__estimate_point", @@ -170,7 +178,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet): & Q(end_date__gte=timezone.now()), then=Value("CURRENT"), ), - When(start_date__gt=timezone.now(), then=Value("UPCOMING")), + When( + start_date__gt=timezone.now(), then=Value("UPCOMING") + ), When(end_date__lt=timezone.now(), then=Value("COMPLETED")), When( Q(start_date__isnull=True) & Q(end_date__isnull=True), @@ -183,13 +193,17 @@ class CycleViewSet(WebhookMixin, BaseViewSet): .prefetch_related( Prefetch( "issue_cycle__issue__assignees", - queryset=User.objects.only("avatar", "first_name", "id").distinct(), + queryset=User.objects.only( + "avatar", "first_name", "id" + ).distinct(), ) ) .prefetch_related( Prefetch( "issue_cycle__issue__labels", - queryset=Label.objects.only("name", "color", "id").distinct(), + queryset=Label.objects.only( + "name", "color", "id" + ).distinct(), ) ) .order_by("-is_favorite", "name") @@ -199,7 +213,11 @@ class CycleViewSet(WebhookMixin, BaseViewSet): def list(self, request, slug, project_id): queryset = self.get_queryset() cycle_view = request.GET.get("cycle_view", "all") - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] queryset = queryset.order_by("-is_favorite", "-created_at") @@ -296,7 +314,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "completion_chart": {}, } if data[0]["start_date"] and data[0]["end_date"]: - data[0]["distribution"]["completion_chart"] = burndown_plot( + data[0]["distribution"][ + "completion_chart" + ] = burndown_plot( queryset=queryset.first(), slug=slug, project_id=project_id, @@ -322,8 +342,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet): project_id=project_id, owned_by=request.user, ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + cycle = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) + serializer = CycleSerializer(cycle) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) else: return Response( { @@ -333,15 +363,22 @@ class CycleViewSet(WebhookMixin, BaseViewSet): ) def partial_update(self, request, slug, project_id, pk): - cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) request_data = request.data - if cycle.end_date is not None and cycle.end_date < timezone.now().date(): + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): if "sort_order" in request_data: # Can only change sort order request_data = { - "sort_order": request_data.get("sort_order", cycle.sort_order) + "sort_order": request_data.get( + "sort_order", cycle.sort_order + ) } else: return Response( @@ -351,7 +388,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - serializer = CycleWriteSerializer(cycle, data=request.data, partial=True) + serializer = CycleWriteSerializer( + cycle, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) @@ -372,7 +411,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet): .annotate(assignee_id=F("assignees__id")) .annotate(avatar=F("assignees__avatar")) .annotate(display_name=F("assignees__display_name")) - .values("first_name", "last_name", "assignee_id", "avatar", "display_name") + .values( + "first_name", + "last_name", + "assignee_id", + "avatar", + "display_name", + ) .annotate( total_issues=Count( "assignee_id", @@ -451,7 +496,10 @@ class CycleViewSet(WebhookMixin, BaseViewSet): if queryset.start_date and queryset.end_date: data["distribution"]["completion_chart"] = burndown_plot( - queryset=queryset, slug=slug, project_id=project_id, cycle_id=pk + queryset=queryset, + slug=slug, + project_id=project_id, + cycle_id=pk, ) return Response( @@ -461,11 +509,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet): def destroy(self, request, slug, project_id, pk): cycle_issues = list( - CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list( - "issue", flat=True - ) + CycleIssue.objects.filter( + cycle_id=self.kwargs.get("pk") + ).values_list("issue", flat=True) + ) + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk ) - cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) issue_activity.delay( type="cycle.activity.deleted", @@ -508,7 +558,9 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): super() .get_queryset() .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("issue_id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -527,13 +579,19 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): @method_decorator(gzip_page) def list(self, request, slug, project_id, cycle_id): - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] order_by = request.GET.get("order_by", "created_at") filters = issue_filters(request.query_params, "GET") issues = ( Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -548,6 +606,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): .prefetch_related("labels") .order_by(order_by) .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) .order_by() @@ -555,13 +615,22 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + subscriber=self.request.user, issue_id=OuterRef("id") + ) + ) + ) ) - serializer = IssueStateSerializer( + serializer = IssueSerializer( issues, many=True, fields=fields if fields else None ) return Response(serializer.data, status=status.HTTP_200_OK) @@ -571,14 +640,18 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): if not len(issues): return Response( - {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, ) cycle = Cycle.objects.get( workspace__slug=slug, project_id=project_id, pk=cycle_id ) - if cycle.end_date is not None and cycle.end_date < timezone.now().date(): + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): return Response( { "error": "The Cycle has already been completed so no new issues can be added" @@ -652,8 +725,12 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): ) # Return all Cycle Issues + issues = self.get_queryset().values_list("issue_id", flat=True) + return Response( - CycleIssueSerializer(self.get_queryset(), many=True).data, + IssueSerializer( + Issue.objects.filter(pk__in=issues), many=True + ).data, status=status.HTTP_200_OK, ) @@ -810,7 +887,9 @@ class CycleUserPropertiesEndpoint(BaseAPIView): workspace__slug=slug, ) - cycle_properties.filters = request.data.get("filters", cycle_properties.filters) + cycle_properties.filters = request.data.get( + "filters", cycle_properties.filters + ) cycle_properties.display_filters = request.data.get( "display_filters", cycle_properties.display_filters ) @@ -831,3 +910,235 @@ class CycleUserPropertiesEndpoint(BaseAPIView): ) serializer = CycleUserPropertiesSerializer(cycle_properties) return Response(serializer.data, status=status.HTTP_200_OK) + + +class ActiveCycleEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceUserPermission, + ] + def get(self, request, slug): + subquery = CycleFavorite.objects.filter( + user=self.request.user, + cycle_id=OuterRef("pk"), + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + ) + active_cycles = ( + Cycle.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=self.request.user, + start_date__lte=timezone.now(), + end_date__gte=timezone.now(), + ) + .select_related("project") + .select_related("workspace") + .select_related("owned_by") + .annotate(is_favorite=Exists(subquery)) + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="cancelled", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="unstarted", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="backlog", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) + .annotate( + completed_estimates=Sum( + "issue_cycle__issue__estimate_point", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_estimates=Sum( + "issue_cycle__issue__estimate_point", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + status=Case( + When( + Q(start_date__lte=timezone.now()) + & Q(end_date__gte=timezone.now()), + then=Value("CURRENT"), + ), + When(start_date__gt=timezone.now(), then=Value("UPCOMING")), + When(end_date__lt=timezone.now(), then=Value("COMPLETED")), + When( + Q(start_date__isnull=True) & Q(end_date__isnull=True), + then=Value("DRAFT"), + ), + default=Value("DRAFT"), + output_field=CharField(), + ) + ) + .prefetch_related( + Prefetch( + "issue_cycle__issue__assignees", + queryset=User.objects.only("avatar", "first_name", "id").distinct(), + ) + ) + .prefetch_related( + Prefetch( + "issue_cycle__issue__labels", + queryset=Label.objects.only("name", "color", "id").distinct(), + ) + ) + .order_by("-created_at") + ) + + cycles = CycleSerializer(active_cycles, many=True).data + + for cycle in cycles: + assignee_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=cycle["id"], + project_id=cycle["project"], + workspace__slug=slug, + ) + .annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .values("display_name", "assignee_id", "avatar") + .annotate( + total_issues=Count( + "assignee_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) + .annotate( + completed_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("display_name") + ) + + label_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=cycle["id"], + project_id=cycle["project"], + workspace__slug=slug, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_issues=Count( + "label_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ) + ) + .annotate( + completed_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + cycle["distribution"] = { + "assignees": assignee_distribution, + "labels": label_distribution, + "completion_chart": {}, + } + if cycle["start_date"] and cycle["end_date"]: + cycle["distribution"][ + "completion_chart" + ] = burndown_plot( + queryset=active_cycles.get(pk=cycle["id"]), + slug=slug, + project_id=cycle["project"], + cycle_id=cycle["id"], + ) + + return Response(cycles, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/dashboard.py b/apiserver/plane/app/views/dashboard.py new file mode 100644 index 000000000..af476a130 --- /dev/null +++ b/apiserver/plane/app/views/dashboard.py @@ -0,0 +1,658 @@ +# Django imports +from django.db.models import ( + Q, + Case, + When, + Value, + CharField, + Count, + F, + Exists, + OuterRef, + Max, + Subquery, + JSONField, + Func, + Prefetch, +) +from django.utils import timezone + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from . import BaseAPIView +from plane.db.models import ( + Issue, + IssueActivity, + ProjectMember, + Widget, + DashboardWidget, + Dashboard, + Project, + IssueLink, + IssueAttachment, + IssueRelation, +) +from plane.app.serializers import ( + IssueActivitySerializer, + IssueSerializer, + DashboardSerializer, + WidgetSerializer, +) +from plane.utils.issue_filters import issue_filters + + +def dashboard_overview_stats(self, request, slug): + assigned_issues = Issue.issue_objects.filter( + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + workspace__slug=slug, + assignees__in=[request.user], + ).count() + + pending_issues_count = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + workspace__slug=slug, + assignees__in=[request.user], + ).count() + + created_issues_count = Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + created_by_id=request.user.id, + ).count() + + completed_issues_count = Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + assignees__in=[request.user], + state__group="completed", + ).count() + + return Response( + { + "assigned_issues_count": assigned_issues, + "pending_issues_count": pending_issues_count, + "completed_issues_count": completed_issues_count, + "created_issues_count": created_issues_count, + }, + status=status.HTTP_200_OK, + ) + + +def dashboard_assigned_issues(self, request, slug): + filters = issue_filters(request.query_params, "GET") + issue_type = request.GET.get("issue_type", None) + + # get all the assigned issues + assigned_issues = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + assignees__in=[request.user], + ) + .filter(**filters) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels") + .prefetch_related( + Prefetch( + "issue_relation", + queryset=IssueRelation.objects.select_related( + "related_issue" + ).select_related("issue"), + ) + ) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .order_by("created_at") + ) + + # Priority Ordering + priority_order = ["urgent", "high", "medium", "low", "none"] + assigned_issues = assigned_issues.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + if issue_type == "completed": + completed_issues_count = assigned_issues.filter( + state__group__in=["completed"] + ).count() + completed_issues = assigned_issues.filter( + state__group__in=["completed"] + )[:5] + return Response( + { + "issues": IssueSerializer( + completed_issues, many=True, expand=self.expand + ).data, + "count": completed_issues_count, + }, + status=status.HTTP_200_OK, + ) + + if issue_type == "overdue": + overdue_issues_count = assigned_issues.filter( + state__group__in=["backlog", "unstarted", "started"], + target_date__lt=timezone.now() + ).count() + overdue_issues = assigned_issues.filter( + state__group__in=["backlog", "unstarted", "started"], + target_date__lt=timezone.now() + )[:5] + return Response( + { + "issues": IssueSerializer( + overdue_issues, many=True, expand=self.expand + ).data, + "count": overdue_issues_count, + }, + status=status.HTTP_200_OK, + ) + + if issue_type == "upcoming": + upcoming_issues_count = assigned_issues.filter( + state__group__in=["backlog", "unstarted", "started"], + target_date__gte=timezone.now() + ).count() + upcoming_issues = assigned_issues.filter( + state__group__in=["backlog", "unstarted", "started"], + target_date__gte=timezone.now() + )[:5] + return Response( + { + "issues": IssueSerializer( + upcoming_issues, many=True, expand=self.expand + ).data, + "count": upcoming_issues_count, + }, + status=status.HTTP_200_OK, + ) + + return Response( + {"error": "Please specify a valid issue type"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +def dashboard_created_issues(self, request, slug): + filters = issue_filters(request.query_params, "GET") + issue_type = request.GET.get("issue_type", None) + + # get all the assigned issues + created_issues = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + created_by=request.user, + ) + .filter(**filters) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .order_by("created_at") + ) + + # Priority Ordering + priority_order = ["urgent", "high", "medium", "low", "none"] + created_issues = created_issues.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + if issue_type == "completed": + completed_issues_count = created_issues.filter( + state__group__in=["completed"] + ).count() + completed_issues = created_issues.filter( + state__group__in=["completed"] + )[:5] + return Response( + { + "issues": IssueSerializer(completed_issues, many=True).data, + "count": completed_issues_count, + }, + status=status.HTTP_200_OK, + ) + + if issue_type == "overdue": + overdue_issues_count = created_issues.filter( + state__group__in=["backlog", "unstarted", "started"], + target_date__lt=timezone.now() + ).count() + overdue_issues = created_issues.filter( + state__group__in=["backlog", "unstarted", "started"], + target_date__lt=timezone.now() + )[:5] + return Response( + { + "issues": IssueSerializer(overdue_issues, many=True).data, + "count": overdue_issues_count, + }, + status=status.HTTP_200_OK, + ) + + if issue_type == "upcoming": + upcoming_issues_count = created_issues.filter( + state__group__in=["backlog", "unstarted", "started"], + target_date__gte=timezone.now() + ).count() + upcoming_issues = created_issues.filter( + state__group__in=["backlog", "unstarted", "started"], + target_date__gte=timezone.now() + )[:5] + return Response( + { + "issues": IssueSerializer(upcoming_issues, many=True).data, + "count": upcoming_issues_count, + }, + status=status.HTTP_200_OK, + ) + + return Response( + {"error": "Please specify a valid issue type"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +def dashboard_issues_by_state_groups(self, request, slug): + filters = issue_filters(request.query_params, "GET") + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + issues_by_state_groups = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + assignees__in=[request.user], + ) + .filter(**filters) + .values("state__group") + .annotate(count=Count("id")) + ) + + # default state + all_groups = {state: 0 for state in state_order} + + # Update counts for existing groups + for entry in issues_by_state_groups: + all_groups[entry["state__group"]] = entry["count"] + + # Prepare output including all groups with their counts + output_data = [ + {"state": group, "count": count} for group, count in all_groups.items() + ] + + return Response(output_data, status=status.HTTP_200_OK) + + +def dashboard_issues_by_priority(self, request, slug): + filters = issue_filters(request.query_params, "GET") + priority_order = ["urgent", "high", "medium", "low", "none"] + + issues_by_priority = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__is_active=True, + project__project_projectmember__member=request.user, + assignees__in=[request.user], + ) + .filter(**filters) + .values("priority") + .annotate(count=Count("id")) + ) + + # default priority + all_groups = {priority: 0 for priority in priority_order} + + # Update counts for existing groups + for entry in issues_by_priority: + all_groups[entry["priority"]] = entry["count"] + + # Prepare output including all groups with their counts + output_data = [ + {"priority": group, "count": count} + for group, count in all_groups.items() + ] + + return Response(output_data, status=status.HTTP_200_OK) + + +def dashboard_recent_activity(self, request, slug): + queryset = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + actor=request.user, + ).select_related("actor", "workspace", "issue", "project")[:8] + + return Response( + IssueActivitySerializer(queryset, many=True).data, + status=status.HTTP_200_OK, + ) + + +def dashboard_recent_projects(self, request, slug): + project_ids = ( + IssueActivity.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + actor=request.user, + ) + .values_list("project_id", flat=True) + .distinct() + ) + + # Extract project IDs from the recent projects + unique_project_ids = set(project_id for project_id in project_ids) + + # Fetch additional projects only if needed + if len(unique_project_ids) < 4: + additional_projects = Project.objects.filter( + project_projectmember__member=request.user, + project_projectmember__is_active=True, + workspace__slug=slug, + ).exclude(id__in=unique_project_ids) + + # Append additional project IDs to the existing list + unique_project_ids.update(additional_projects.values_list("id", flat=True)) + + return Response( + list(unique_project_ids)[:4], + status=status.HTTP_200_OK, + ) + + +def dashboard_recent_collaborators(self, request, slug): + # Fetch all project IDs where the user belongs to + user_projects = Project.objects.filter( + project_projectmember__member=request.user, + project_projectmember__is_active=True, + workspace__slug=slug, + ).values_list("id", flat=True) + + # Fetch all users who have performed an activity in the projects where the user exists + users_with_activities = ( + IssueActivity.objects.filter( + workspace__slug=slug, + project_id__in=user_projects, + ) + .values("actor") + .exclude(actor=request.user) + .annotate(num_activities=Count("actor")) + .order_by("-num_activities") + )[:7] + + # Get the count of active issues for each user in users_with_activities + users_with_active_issues = [] + for user_activity in users_with_activities: + user_id = user_activity["actor"] + active_issue_count = Issue.objects.filter( + assignees__in=[user_id], + state__group__in=["unstarted", "started"], + ).count() + users_with_active_issues.append( + {"user_id": user_id, "active_issue_count": active_issue_count} + ) + + # Insert the logged-in user's ID and their active issue count at the beginning + active_issue_count = Issue.objects.filter( + assignees__in=[request.user], + state__group__in=["unstarted", "started"], + ).count() + + if users_with_activities.count() < 7: + # Calculate the additional collaborators needed + additional_collaborators_needed = 7 - users_with_activities.count() + + # Fetch additional collaborators from the project_member table + additional_collaborators = list( + set( + ProjectMember.objects.filter( + ~Q(member=request.user), + project_id__in=user_projects, + workspace__slug=slug, + ) + .exclude( + member__in=[ + user["actor"] for user in users_with_activities + ] + ) + .values_list("member", flat=True) + ) + ) + + additional_collaborators = additional_collaborators[ + :additional_collaborators_needed + ] + + # Append additional collaborators to the list + for collaborator_id in additional_collaborators: + active_issue_count = Issue.objects.filter( + assignees__in=[collaborator_id], + state__group__in=["unstarted", "started"], + ).count() + users_with_active_issues.append( + { + "user_id": str(collaborator_id), + "active_issue_count": active_issue_count, + } + ) + + users_with_active_issues.insert( + 0, + {"user_id": request.user.id, "active_issue_count": active_issue_count}, + ) + + return Response(users_with_active_issues, status=status.HTTP_200_OK) + + +class DashboardEndpoint(BaseAPIView): + def create(self, request, slug): + serializer = DashboardSerializer(data=request.data) + 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) + + def patch(self, request, slug, pk): + serializer = DashboardSerializer(data=request.data, partial=True) + 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): + serializer = DashboardSerializer(data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def get(self, request, slug, dashboard_id=None): + if not dashboard_id: + dashboard_type = request.GET.get("dashboard_type", None) + if dashboard_type == "home": + dashboard, created = Dashboard.objects.get_or_create( + type_identifier=dashboard_type, owned_by=request.user, is_default=True + ) + + if created: + widgets_to_fetch = [ + "overview_stats", + "assigned_issues", + "created_issues", + "issues_by_state_groups", + "issues_by_priority", + "recent_activity", + "recent_projects", + "recent_collaborators", + ] + + updated_dashboard_widgets = [] + for widget_key in widgets_to_fetch: + widget = Widget.objects.filter(key=widget_key).values_list("id", flat=True) + if widget: + updated_dashboard_widgets.append( + DashboardWidget( + widget_id=widget, + dashboard_id=dashboard.id, + ) + ) + + DashboardWidget.objects.bulk_create( + updated_dashboard_widgets, batch_size=100 + ) + + widgets = ( + Widget.objects.annotate( + is_visible=Exists( + DashboardWidget.objects.filter( + widget_id=OuterRef("pk"), + dashboard_id=dashboard.id, + is_visible=True, + ) + ) + ) + .annotate( + dashboard_filters=Subquery( + DashboardWidget.objects.filter( + widget_id=OuterRef("pk"), + dashboard_id=dashboard.id, + filters__isnull=False, + ) + .exclude(filters={}) + .values("filters")[:1] + ) + ) + .annotate( + widget_filters=Case( + When( + dashboard_filters__isnull=False, + then=F("dashboard_filters"), + ), + default=F("filters"), + output_field=JSONField(), + ) + ) + ) + return Response( + { + "dashboard": DashboardSerializer(dashboard).data, + "widgets": WidgetSerializer(widgets, many=True).data, + }, + status=status.HTTP_200_OK, + ) + return Response( + {"error": "Please specify a valid dashboard type"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + widget_key = request.GET.get("widget_key", "overview_stats") + + WIDGETS_MAPPER = { + "overview_stats": dashboard_overview_stats, + "assigned_issues": dashboard_assigned_issues, + "created_issues": dashboard_created_issues, + "issues_by_state_groups": dashboard_issues_by_state_groups, + "issues_by_priority": dashboard_issues_by_priority, + "recent_activity": dashboard_recent_activity, + "recent_projects": dashboard_recent_projects, + "recent_collaborators": dashboard_recent_collaborators, + } + + func = WIDGETS_MAPPER.get(widget_key) + if func is not None: + response = func( + self, + request=request, + slug=slug, + ) + if isinstance(response, Response): + return response + + return Response( + {"error": "Please specify a valid widget key"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class WidgetsEndpoint(BaseAPIView): + def patch(self, request, dashboard_id, widget_id): + dashboard_widget = DashboardWidget.objects.filter( + widget_id=widget_id, + dashboard_id=dashboard_id, + ).first() + dashboard_widget.is_visible = request.data.get( + "is_visible", dashboard_widget.is_visible + ) + dashboard_widget.sort_order = request.data.get( + "sort_order", dashboard_widget.sort_order + ) + dashboard_widget.filters = request.data.get( + "filters", dashboard_widget.filters + ) + dashboard_widget.save() + return Response( + {"message": "successfully updated"}, status=status.HTTP_200_OK + ) diff --git a/apiserver/plane/app/views/estimate.py b/apiserver/plane/app/views/estimate.py index 8f14b230b..3402bb068 100644 --- a/apiserver/plane/app/views/estimate.py +++ b/apiserver/plane/app/views/estimate.py @@ -19,16 +19,16 @@ class ProjectEstimatePointEndpoint(BaseAPIView): ] def get(self, request, slug, project_id): - project = Project.objects.get(workspace__slug=slug, pk=project_id) - if project.estimate_id is not None: - estimate_points = EstimatePoint.objects.filter( - estimate_id=project.estimate_id, - project_id=project_id, - workspace__slug=slug, - ) - serializer = EstimatePointSerializer(estimate_points, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response([], status=status.HTTP_200_OK) + project = Project.objects.get(workspace__slug=slug, pk=project_id) + if project.estimate_id is not None: + estimate_points = EstimatePoint.objects.filter( + estimate_id=project.estimate_id, + project_id=project_id, + workspace__slug=slug, + ) + serializer = EstimatePointSerializer(estimate_points, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response([], status=status.HTTP_200_OK) class BulkEstimatePointEndpoint(BaseViewSet): @@ -39,9 +39,13 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer_class = EstimateSerializer def list(self, request, slug, project_id): - estimates = Estimate.objects.filter( - workspace__slug=slug, project_id=project_id - ).prefetch_related("points").select_related("workspace", "project") + estimates = ( + Estimate.objects.filter( + workspace__slug=slug, project_id=project_id + ) + .prefetch_related("points") + .select_related("workspace", "project") + ) serializer = EstimateReadSerializer(estimates, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @@ -53,14 +57,18 @@ class BulkEstimatePointEndpoint(BaseViewSet): ) estimate_points = request.data.get("estimate_points", []) - - serializer = EstimatePointSerializer(data=request.data.get("estimate_points"), many=True) + + serializer = EstimatePointSerializer( + data=request.data.get("estimate_points"), many=True + ) if not serializer.is_valid(): return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - estimate_serializer = EstimateSerializer(data=request.data.get("estimate")) + estimate_serializer = EstimateSerializer( + data=request.data.get("estimate") + ) if not estimate_serializer.is_valid(): return Response( estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST @@ -135,7 +143,8 @@ class BulkEstimatePointEndpoint(BaseViewSet): estimate_points = EstimatePoint.objects.filter( pk__in=[ - estimate_point.get("id") for estimate_point in estimate_points_data + estimate_point.get("id") + for estimate_point in estimate_points_data ], workspace__slug=slug, project_id=project_id, @@ -157,10 +166,14 @@ class BulkEstimatePointEndpoint(BaseViewSet): updated_estimate_points.append(estimate_point) EstimatePoint.objects.bulk_update( - updated_estimate_points, ["value"], batch_size=10, + updated_estimate_points, + ["value"], + batch_size=10, ) - estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True) + estimate_point_serializer = EstimatePointSerializer( + estimate_points, many=True + ) return Response( { "estimate": estimate_serializer.data, diff --git a/apiserver/plane/app/views/exporter.py b/apiserver/plane/app/views/exporter.py index b709a599d..179de81f9 100644 --- a/apiserver/plane/app/views/exporter.py +++ b/apiserver/plane/app/views/exporter.py @@ -21,11 +21,11 @@ class ExportIssuesEndpoint(BaseAPIView): def post(self, request, slug): # Get the workspace workspace = Workspace.objects.get(slug=slug) - + provider = request.data.get("provider", False) multiple = request.data.get("multiple", False) project_ids = request.data.get("project", []) - + if provider in ["csv", "xlsx", "json"]: if not project_ids: project_ids = Project.objects.filter( @@ -63,9 +63,11 @@ class ExportIssuesEndpoint(BaseAPIView): def get(self, request, slug): exporter_history = ExporterHistory.objects.filter( workspace__slug=slug - ).select_related("workspace","initiated_by") + ).select_related("workspace", "initiated_by") - if request.GET.get("per_page", False) and request.GET.get("cursor", False): + if request.GET.get("per_page", False) and request.GET.get( + "cursor", False + ): return self.paginate( request=request, queryset=exporter_history, diff --git a/apiserver/plane/app/views/external.py b/apiserver/plane/app/views/external.py index 97d509c1e..618c65e3c 100644 --- a/apiserver/plane/app/views/external.py +++ b/apiserver/plane/app/views/external.py @@ -14,7 +14,10 @@ from django.conf import settings from .base import BaseAPIView from plane.app.permissions import ProjectEntityPermission from plane.db.models import Workspace, Project -from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer +from plane.app.serializers import ( + ProjectLiteSerializer, + WorkspaceLiteSerializer, +) from plane.utils.integrations.github import get_release_notes from plane.license.utils.instance_value import get_configuration_value @@ -51,7 +54,8 @@ class GPTIntegrationEndpoint(BaseAPIView): if not task: return Response( - {"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Task is required"}, + status=status.HTTP_400_BAD_REQUEST, ) final_text = task + "\n" + prompt @@ -89,7 +93,7 @@ class ReleaseNotesEndpoint(BaseAPIView): class UnsplashEndpoint(BaseAPIView): def get(self, request): - UNSPLASH_ACCESS_KEY, = get_configuration_value( + (UNSPLASH_ACCESS_KEY,) = get_configuration_value( [ { "key": "UNSPLASH_ACCESS_KEY", diff --git a/apiserver/plane/app/views/importer.py b/apiserver/plane/app/views/importer.py index b99d663e2..a15ed36b7 100644 --- a/apiserver/plane/app/views/importer.py +++ b/apiserver/plane/app/views/importer.py @@ -35,14 +35,16 @@ from plane.app.serializers import ( ModuleSerializer, ) from plane.utils.integrations.github import get_github_repo_details -from plane.utils.importers.jira import jira_project_issue_summary +from plane.utils.importers.jira import ( + jira_project_issue_summary, + is_allowed_hostname, +) from plane.bgtasks.importer_task import service_importer from plane.utils.html_processor import strip_tags from plane.app.permissions import WorkSpaceAdminPermission class ServiceIssueImportSummaryEndpoint(BaseAPIView): - def get(self, request, slug, service): if service == "github": owner = request.GET.get("owner", False) @@ -94,7 +96,8 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView): for key, error_message in params.items(): if not request.GET.get(key, False): return Response( - {"error": error_message}, status=status.HTTP_400_BAD_REQUEST + {"error": error_message}, + status=status.HTTP_400_BAD_REQUEST, ) project_key = request.GET.get("project_key", "") @@ -122,6 +125,7 @@ class ImportServiceEndpoint(BaseAPIView): permission_classes = [ WorkSpaceAdminPermission, ] + def post(self, request, slug, service): project_id = request.data.get("project_id", False) @@ -174,6 +178,21 @@ class ImportServiceEndpoint(BaseAPIView): data = request.data.get("data", False) metadata = request.data.get("metadata", False) config = request.data.get("config", False) + + cloud_hostname = metadata.get("cloud_hostname", False) + + if not cloud_hostname: + return Response( + {"error": "Cloud hostname is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if not is_allowed_hostname(cloud_hostname): + return Response( + {"error": "Hostname is not a valid hostname."}, + status=status.HTTP_400_BAD_REQUEST, + ) + if not data or not metadata: return Response( {"error": "Data, config and metadata are required"}, @@ -244,7 +263,9 @@ class ImportServiceEndpoint(BaseAPIView): importer = Importer.objects.get( pk=pk, service=service, workspace__slug=slug ) - serializer = ImporterSerializer(importer, data=request.data, partial=True) + serializer = ImporterSerializer( + importer, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) @@ -280,9 +301,9 @@ class BulkImportIssuesEndpoint(BaseAPIView): ).first() # Get the maximum sequence_id - last_id = IssueSequence.objects.filter(project_id=project_id).aggregate( - largest=Max("sequence") - )["largest"] + last_id = IssueSequence.objects.filter( + project_id=project_id + ).aggregate(largest=Max("sequence"))["largest"] last_id = 1 if last_id is None else last_id + 1 @@ -315,7 +336,9 @@ class BulkImportIssuesEndpoint(BaseAPIView): if issue_data.get("state", False) else default_state.id, name=issue_data.get("name", "Issue Created through Bulk"), - description_html=issue_data.get("description_html", "

"), + description_html=issue_data.get( + "description_html", "

" + ), description_stripped=( None if ( @@ -427,15 +450,21 @@ class BulkImportIssuesEndpoint(BaseAPIView): for comment in comments_list ] - _ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100) + _ = IssueComment.objects.bulk_create( + bulk_issue_comments, batch_size=100 + ) # Attach Links _ = IssueLink.objects.bulk_create( [ IssueLink( issue=issue, - url=issue_data.get("link", {}).get("url", "https://github.com"), - title=issue_data.get("link", {}).get("title", "Original Issue"), + url=issue_data.get("link", {}).get( + "url", "https://github.com" + ), + title=issue_data.get("link", {}).get( + "title", "Original Issue" + ), project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, @@ -472,7 +501,9 @@ class BulkImportModulesEndpoint(BaseAPIView): ignore_conflicts=True, ) - modules = Module.objects.filter(id__in=[module.id for module in modules]) + modules = Module.objects.filter( + id__in=[module.id for module in modules] + ) if len(modules) == len(modules_data): _ = ModuleLink.objects.bulk_create( @@ -520,6 +551,8 @@ class BulkImportModulesEndpoint(BaseAPIView): else: return Response( - {"message": "Modules created but issues could not be imported"}, + { + "message": "Modules created but issues could not be imported" + }, status=status.HTTP_200_OK, ) diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index 32f38d97c..ff88bfdab 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -62,7 +62,9 @@ class InboxViewSet(BaseViewSet): serializer.save(project_id=self.kwargs.get("project_id")) def destroy(self, request, slug, project_id, pk): - inbox = Inbox.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + inbox = Inbox.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) # Handle default inbox delete if inbox.is_default: return Response( @@ -90,7 +92,8 @@ class InboxIssueViewSet(BaseViewSet): super() .get_queryset() .filter( - Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + Q(snoozed_till__gte=timezone.now()) + | Q(snoozed_till__isnull=True), workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"), inbox_id=self.kwargs.get("inbox_id"), @@ -111,7 +114,9 @@ class InboxIssueViewSet(BaseViewSet): .prefetch_related("assignees", "labels") .order_by("issue_inbox__snoozed_till", "issue_inbox__status") .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -123,7 +128,9 @@ class InboxIssueViewSet(BaseViewSet): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -146,7 +153,8 @@ class InboxIssueViewSet(BaseViewSet): def create(self, request, slug, project_id, inbox_id): if not request.data.get("issue", {}).get("name", False): return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, ) # Check for valid priority @@ -158,7 +166,8 @@ class InboxIssueViewSet(BaseViewSet): "none", ]: return Response( - {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Invalid priority"}, + status=status.HTTP_400_BAD_REQUEST, ) # Create or get state @@ -205,7 +214,10 @@ class InboxIssueViewSet(BaseViewSet): def partial_update(self, request, slug, project_id, inbox_id, issue_id): inbox_issue = InboxIssue.objects.get( - issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + issue_id=issue_id, + workspace__slug=slug, + project_id=project_id, + inbox_id=inbox_id, ) # Get the project member project_member = ProjectMember.objects.get( @@ -228,7 +240,9 @@ class InboxIssueViewSet(BaseViewSet): if bool(issue_data): issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + pk=inbox_issue.issue_id, + workspace__slug=slug, + project_id=project_id, ) # Only allow guests and viewers to edit name and description if project_member.role <= 10: @@ -238,7 +252,9 @@ class InboxIssueViewSet(BaseViewSet): "description_html": issue_data.get( "description_html", issue.description_html ), - "description": issue_data.get("description", issue.description), + "description": issue_data.get( + "description", issue.description + ), } issue_serializer = IssueCreateSerializer( @@ -284,7 +300,9 @@ class InboxIssueViewSet(BaseViewSet): project_id=project_id, ) state = State.objects.filter( - group="cancelled", workspace__slug=slug, project_id=project_id + group="cancelled", + workspace__slug=slug, + project_id=project_id, ).first() if state is not None: issue.state = state @@ -302,17 +320,22 @@ class InboxIssueViewSet(BaseViewSet): if issue.state.name == "Triage": # Move to default state state = State.objects.filter( - workspace__slug=slug, project_id=project_id, default=True + workspace__slug=slug, + project_id=project_id, + default=True, ).first() if state is not None: issue.state = state issue.save() return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) else: return Response( - InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK + InboxIssueSerializer(inbox_issue).data, + status=status.HTTP_200_OK, ) def retrieve(self, request, slug, project_id, inbox_id, issue_id): @@ -324,7 +347,10 @@ class InboxIssueViewSet(BaseViewSet): def destroy(self, request, slug, project_id, inbox_id, issue_id): inbox_issue = InboxIssue.objects.get( - issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + issue_id=issue_id, + workspace__slug=slug, + project_id=project_id, + inbox_id=inbox_id, ) # Get the project member project_member = ProjectMember.objects.get( @@ -351,4 +377,3 @@ class InboxIssueViewSet(BaseViewSet): inbox_issue.delete() return Response(status=status.HTTP_204_NO_CONTENT) - diff --git a/apiserver/plane/app/views/integration/base.py b/apiserver/plane/app/views/integration/base.py index b82957dfb..d757fe471 100644 --- a/apiserver/plane/app/views/integration/base.py +++ b/apiserver/plane/app/views/integration/base.py @@ -1,6 +1,7 @@ # Python improts import uuid import requests + # Django imports from django.contrib.auth.hashers import make_password @@ -19,7 +20,10 @@ from plane.db.models import ( WorkspaceMember, APIToken, ) -from plane.app.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer +from plane.app.serializers import ( + IntegrationSerializer, + WorkspaceIntegrationSerializer, +) from plane.utils.integrations.github import ( get_github_metadata, delete_github_installation, @@ -27,6 +31,7 @@ from plane.utils.integrations.github import ( from plane.app.permissions import WorkSpaceAdminPermission from plane.utils.integrations.slack import slack_oauth + class IntegrationViewSet(BaseViewSet): serializer_class = IntegrationSerializer model = Integration @@ -101,7 +106,10 @@ class WorkspaceIntegrationViewSet(BaseViewSet): code = request.data.get("code", False) if not code: - return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": "Code is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) slack_response = slack_oauth(code=code) @@ -110,7 +118,9 @@ class WorkspaceIntegrationViewSet(BaseViewSet): team_id = metadata.get("team", {}).get("id", False) if not metadata or not access_token or not team_id: return Response( - {"error": "Slack could not be installed. Please try again later"}, + { + "error": "Slack could not be installed. Please try again later" + }, status=status.HTTP_400_BAD_REQUEST, ) config = {"team_id": team_id, "access_token": access_token} diff --git a/apiserver/plane/app/views/integration/github.py b/apiserver/plane/app/views/integration/github.py index 29b7a9b2f..2d37c64b0 100644 --- a/apiserver/plane/app/views/integration/github.py +++ b/apiserver/plane/app/views/integration/github.py @@ -21,7 +21,10 @@ from plane.app.serializers import ( GithubCommentSyncSerializer, ) from plane.utils.integrations.github import get_github_repos -from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission +from plane.app.permissions import ( + ProjectBasePermission, + ProjectEntityPermission, +) class GithubRepositoriesEndpoint(BaseAPIView): @@ -185,11 +188,10 @@ class BulkCreateGithubIssueSyncEndpoint(BaseAPIView): class GithubCommentSyncViewSet(BaseViewSet): - permission_classes = [ ProjectEntityPermission, ] - + serializer_class = GithubCommentSyncSerializer model = GithubCommentSync diff --git a/apiserver/plane/app/views/integration/slack.py b/apiserver/plane/app/views/integration/slack.py index 3f18a2ab2..410e6b332 100644 --- a/apiserver/plane/app/views/integration/slack.py +++ b/apiserver/plane/app/views/integration/slack.py @@ -8,9 +8,16 @@ from sentry_sdk import capture_exception # Module imports from plane.app.views import BaseViewSet, BaseAPIView -from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember +from plane.db.models import ( + SlackProjectSync, + WorkspaceIntegration, + ProjectMember, +) from plane.app.serializers import SlackProjectSyncSerializer -from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission +from plane.app.permissions import ( + ProjectBasePermission, + ProjectEntityPermission, +) from plane.utils.integrations.slack import slack_oauth @@ -38,7 +45,8 @@ class SlackProjectSyncViewSet(BaseViewSet): if not code: return Response( - {"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Code is required"}, + status=status.HTTP_400_BAD_REQUEST, ) slack_response = slack_oauth(code=code) @@ -54,7 +62,9 @@ class SlackProjectSyncViewSet(BaseViewSet): access_token=slack_response.get("access_token"), scopes=slack_response.get("scope"), bot_user_id=slack_response.get("bot_user_id"), - webhook_url=slack_response.get("incoming_webhook", {}).get("url"), + webhook_url=slack_response.get("incoming_webhook", {}).get( + "url" + ), data=slack_response, team_id=slack_response.get("team", {}).get("id"), team_name=slack_response.get("team", {}).get("name"), @@ -62,7 +72,9 @@ class SlackProjectSyncViewSet(BaseViewSet): project_id=project_id, ) _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, role=20, project_id=project_id + member=workspace_integration.actor, + role=20, + project_id=project_id, ) serializer = SlackProjectSyncSerializer(slack_project_sync) return Response(serializer.data, status=status.HTTP_200_OK) @@ -74,6 +86,8 @@ class SlackProjectSyncViewSet(BaseViewSet): ) capture_exception(e) return Response( - {"error": "Slack could not be installed. Please try again later"}, + { + "error": "Slack could not be installed. Please try again later" + }, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 6c88ef090..ec8b4da5e 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -34,11 +34,11 @@ from rest_framework.parsers import MultiPartParser, FormParser # Module imports from . import BaseViewSet, BaseAPIView, WebhookMixin from plane.app.serializers import ( - IssueCreateSerializer, IssueActivitySerializer, IssueCommentSerializer, IssuePropertySerializer, IssueSerializer, + IssueCreateSerializer, LabelSerializer, IssueFlatSerializer, IssueLinkSerializer, @@ -52,7 +52,6 @@ from plane.app.serializers import ( IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, - IssueRelationLiteSerializer, ) from plane.app.permissions import ( ProjectEntityPermission, @@ -82,7 +81,7 @@ from plane.db.models import ( from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters - +from collections import defaultdict class IssueViewSet(WebhookMixin, BaseViewSet): def get_serializer_class(self): @@ -110,13 +109,9 @@ class IssueViewSet(WebhookMixin, BaseViewSet): def get_queryset(self): return ( - 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") + Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id") ) - .filter(project_id=self.kwargs.get("project_id")) .filter(workspace__slug=self.kwargs.get("slug")) .select_related("project") .select_related("workspace") @@ -139,17 +134,20 @@ class IssueViewSet(WebhookMixin, BaseViewSet): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - subscriber=self.request.user, issue_id=OuterRef("id") - ) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) ).distinct() @@ -159,7 +157,13 @@ class IssueViewSet(WebhookMixin, BaseViewSet): # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] order_by_param = request.GET.get("order_by", "-created_at") @@ -168,7 +172,9 @@ class IssueViewSet(WebhookMixin, BaseViewSet): # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( - priority_order if order_by_param == "priority" else priority_order[::-1] + priority_order + if order_by_param == "priority" + else priority_order[::-1] ) issue_queryset = issue_queryset.annotate( priority_order=Case( @@ -216,7 +222,9 @@ class IssueViewSet(WebhookMixin, BaseViewSet): else order_by_param ) ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + "-max_values" + if order_by_param.startswith("-") + else "max_values" ) else: issue_queryset = issue_queryset.order_by(order_by_param) @@ -244,35 +252,42 @@ class IssueViewSet(WebhookMixin, BaseViewSet): # Track the issue issue_activity.delay( type="issue.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), actor_id=str(request.user.id), issue_id=str(serializer.data.get("id", None)), project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) + issue = ( + self.get_queryset().filter(pk=serializer.data["id"]).first() + ) + serializer = IssueSerializer(issue) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk=None): - issue = 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") - ).get(workspace__slug=slug, project_id=project_id, pk=pk) + issue = self.get_queryset().filter(pk=pk).first() return Response( - IssueSerializer(issue, fields=self.fields, expand=self.expand).data, + IssueSerializer( + issue, fields=self.fields, expand=self.expand + ).data, status=status.HTTP_200_OK, ) def partial_update(self, request, slug, project_id, pk=None): - issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) current_instance = json.dumps( IssueSerializer(issue).data, cls=DjangoJSONEncoder ) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - serializer = IssueCreateSerializer(issue, data=request.data, partial=True) + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() issue_activity.delay( @@ -284,11 +299,16 @@ class IssueViewSet(WebhookMixin, BaseViewSet): current_instance=current_instance, epoch=int(timezone.now().timestamp()), ) - return Response(serializer.data, status=status.HTTP_200_OK) + issue = self.get_queryset().filter(pk=pk).first() + return Response( + IssueSerializer(issue).data, status=status.HTTP_200_OK + ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, slug, project_id, pk=None): - issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) current_instance = json.dumps( IssueSerializer(issue).data, cls=DjangoJSONEncoder ) @@ -311,7 +331,13 @@ class UserWorkSpaceIssues(BaseAPIView): filters = issue_filters(request.query_params, "GET") # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] order_by_param = request.GET.get("order_by", "-created_at") @@ -325,7 +351,9 @@ class UserWorkSpaceIssues(BaseAPIView): workspace__slug=slug, ) .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -344,7 +372,9 @@ class UserWorkSpaceIssues(BaseAPIView): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -361,7 +391,9 @@ class UserWorkSpaceIssues(BaseAPIView): # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( - priority_order if order_by_param == "priority" else priority_order[::-1] + priority_order + if order_by_param == "priority" + else priority_order[::-1] ) issue_queryset = issue_queryset.annotate( priority_order=Case( @@ -409,7 +441,9 @@ class UserWorkSpaceIssues(BaseAPIView): else order_by_param ) ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + "-max_values" + if order_by_param.startswith("-") + else "max_values" ) else: issue_queryset = issue_queryset.order_by(order_by_param) @@ -478,7 +512,9 @@ class IssueActivityEndpoint(BaseAPIView): ) ) ) - issue_activities = IssueActivitySerializer(issue_activities, many=True).data + issue_activities = IssueActivitySerializer( + issue_activities, many=True + ).data issue_comments = IssueCommentSerializer(issue_comments, many=True).data result_list = sorted( @@ -536,7 +572,9 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet): ) issue_activity.delay( type="comment.activity.created", - requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id")), project_id=str(self.kwargs.get("project_id")), @@ -548,7 +586,10 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet): def partial_update(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 + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, ) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) current_instance = json.dumps( @@ -574,7 +615,10 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet): def destroy(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 + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, ) current_instance = json.dumps( IssueCommentSerializer(issue_comment).data, @@ -604,7 +648,9 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView): project_id=project_id, ) - issue_property.filters = request.data.get("filters", issue_property.filters) + issue_property.filters = request.data.get( + "filters", issue_property.filters + ) issue_property.display_filters = request.data.get( "display_filters", issue_property.display_filters ) @@ -635,11 +681,17 @@ class LabelViewSet(BaseViewSet): serializer = LabelSerializer(data=request.data) if serializer.is_valid(): serializer.save(project_id=project_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) except IntegrityError: return Response( - {"error": "Label with the same name already exists in the project"}, + { + "error": "Label with the same name already exists in the project" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -694,7 +746,9 @@ class SubIssuesEndpoint(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug, project_id, issue_id): sub_issues = ( - Issue.issue_objects.filter(parent_id=issue_id, workspace__slug=slug) + Issue.issue_objects.filter( + parent_id=issue_id, workspace__slug=slug + ) .select_related("project") .select_related("workspace") .select_related("state") @@ -702,7 +756,9 @@ class SubIssuesEndpoint(BaseAPIView): .prefetch_related("assignees") .prefetch_related("labels") .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -714,37 +770,26 @@ class SubIssuesEndpoint(BaseAPIView): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - subscriber=self.request.user, issue_id=OuterRef("id") - ) - ) - ) .prefetch_related( Prefetch( "issue_reactions", queryset=IssueReaction.objects.select_related("actor"), ) ) + .annotate(state_group=F("state__group")) ) - state_distribution = ( - State.objects.filter(workspace__slug=slug, state_issue__parent_id=issue_id) - .annotate(state_group=F("group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) - - result = { - item["state_group"]: item["state_count"] for item in state_distribution - } + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) serializer = IssueSerializer( sub_issues, @@ -776,7 +821,7 @@ class SubIssuesEndpoint(BaseAPIView): _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) - updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) + updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(state_group=F("state__group")) # Track the issue _ = [ @@ -791,11 +836,24 @@ class SubIssuesEndpoint(BaseAPIView): ) for sub_issue_id in sub_issue_ids ] + + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in updated_sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) + serializer = IssueSerializer( + updated_sub_issues, + many=True, + ) return Response( - IssueSerializer(updated_sub_issues, many=True).data, + { + "sub_issues": serializer.data, + "state_distribution": result, + }, status=status.HTTP_200_OK, ) + class IssueLinkViewSet(BaseViewSet): @@ -827,7 +885,9 @@ class IssueLinkViewSet(BaseViewSet): ) issue_activity.delay( type="link.activity.created", - requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id")), project_id=str(self.kwargs.get("project_id")), @@ -839,14 +899,19 @@ class IssueLinkViewSet(BaseViewSet): def partial_update(self, request, slug, project_id, issue_id, pk): issue_link = IssueLink.objects.get( - workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, ) requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) current_instance = json.dumps( IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder, ) - serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True) + serializer = IssueLinkSerializer( + issue_link, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() issue_activity.delay( @@ -863,7 +928,10 @@ class IssueLinkViewSet(BaseViewSet): def destroy(self, request, slug, project_id, issue_id, pk): issue_link = IssueLink.objects.get( - workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, ) current_instance = json.dumps( IssueLinkSerializer(issue_link).data, @@ -989,13 +1057,23 @@ class IssueArchiveViewSet(BaseViewSet): @method_decorator(gzip_page) def list(self, request, slug, project_id): - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] filters = issue_filters(request.query_params, "GET") show_sub_issues = request.GET.get("show_sub_issues", "true") # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] order_by_param = request.GET.get("order_by", "-created_at") @@ -1011,7 +1089,9 @@ class IssueArchiveViewSet(BaseViewSet): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -1021,7 +1101,9 @@ class IssueArchiveViewSet(BaseViewSet): # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( - priority_order if order_by_param == "priority" else priority_order[::-1] + priority_order + if order_by_param == "priority" + else priority_order[::-1] ) issue_queryset = issue_queryset.annotate( priority_order=Case( @@ -1069,7 +1151,9 @@ class IssueArchiveViewSet(BaseViewSet): else order_by_param ) ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + "-max_values" + if order_by_param.startswith("-") + else "max_values" ) else: issue_queryset = issue_queryset.order_by(order_by_param) @@ -1080,7 +1164,7 @@ class IssueArchiveViewSet(BaseViewSet): else issue_queryset.filter(parent__isnull=True) ) - issues = IssueLiteSerializer( + issues = IssueSerializer( issue_queryset, many=True, fields=fields if fields else None ).data return Response(issues, status=status.HTTP_200_OK) @@ -1157,24 +1241,11 @@ class IssueSubscriberViewSet(BaseViewSet): ) def list(self, request, slug, project_id, issue_id): - members = ( - ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - is_active=True, - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - subscriber=OuterRef("member"), - ) - ) - ) - .select_related("member") - ) + members = ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + is_active=True, + ).select_related("member") serializer = ProjectMemberLiteSerializer(members, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @@ -1229,7 +1300,9 @@ class IssueSubscriberViewSet(BaseViewSet): workspace__slug=slug, project=project_id, ).exists() - return Response({"subscribed": issue_subscriber}, status=status.HTTP_200_OK) + return Response( + {"subscribed": issue_subscriber}, status=status.HTTP_200_OK + ) class IssueReactionViewSet(BaseViewSet): @@ -1386,7 +1459,9 @@ class IssueRelationViewSet(BaseViewSet): def list(self, request, slug, project_id, issue_id): issue_relations = ( - IssueRelation.objects.filter(Q(issue_id=issue_id) | Q(related_issue=issue_id)) + IssueRelation.objects.filter( + Q(issue_id=issue_id) | Q(related_issue=issue_id) + ) .filter(workspace__slug=self.kwargs.get("slug")) .select_related("project") .select_related("workspace") @@ -1395,34 +1470,59 @@ class IssueRelationViewSet(BaseViewSet): .distinct() ) - blocking_issues = issue_relations.filter(relation_type="blocked_by", related_issue_id=issue_id) - blocked_by_issues = issue_relations.filter(relation_type="blocked_by", issue_id=issue_id) - duplicate_issues = issue_relations.filter(issue_id=issue_id, relation_type="duplicate") - duplicate_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="duplicate") - relates_to_issues = issue_relations.filter(issue_id=issue_id, relation_type="relates_to") - relates_to_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="relates_to") + blocking_issues = issue_relations.filter( + relation_type="blocked_by", related_issue_id=issue_id + ) + blocked_by_issues = issue_relations.filter( + relation_type="blocked_by", issue_id=issue_id + ) + duplicate_issues = issue_relations.filter( + issue_id=issue_id, relation_type="duplicate" + ) + duplicate_issues_related = issue_relations.filter( + related_issue_id=issue_id, relation_type="duplicate" + ) + relates_to_issues = issue_relations.filter( + issue_id=issue_id, relation_type="relates_to" + ) + relates_to_issues_related = issue_relations.filter( + related_issue_id=issue_id, relation_type="relates_to" + ) - blocked_by_issues_serialized = IssueRelationSerializer(blocked_by_issues, many=True).data - duplicate_issues_serialized = IssueRelationSerializer(duplicate_issues, many=True).data - relates_to_issues_serialized = IssueRelationSerializer(relates_to_issues, many=True).data + blocked_by_issues_serialized = IssueRelationSerializer( + blocked_by_issues, many=True + ).data + duplicate_issues_serialized = IssueRelationSerializer( + duplicate_issues, many=True + ).data + relates_to_issues_serialized = IssueRelationSerializer( + relates_to_issues, many=True + ).data # revere relation for blocked by issues - blocking_issues_serialized = RelatedIssueSerializer(blocking_issues, many=True).data + blocking_issues_serialized = RelatedIssueSerializer( + blocking_issues, many=True + ).data # reverse relation for duplicate issues - duplicate_issues_related_serialized = RelatedIssueSerializer(duplicate_issues_related, many=True).data + duplicate_issues_related_serialized = RelatedIssueSerializer( + duplicate_issues_related, many=True + ).data # reverse relation for related issues - relates_to_issues_related_serialized = RelatedIssueSerializer(relates_to_issues_related, many=True).data + relates_to_issues_related_serialized = RelatedIssueSerializer( + relates_to_issues_related, many=True + ).data response_data = { - 'blocking': blocking_issues_serialized, - 'blocked_by': blocked_by_issues_serialized, - 'duplicate': duplicate_issues_serialized + duplicate_issues_related_serialized, - 'relates_to': relates_to_issues_serialized + relates_to_issues_related_serialized, + "blocking": blocking_issues_serialized, + "blocked_by": blocked_by_issues_serialized, + "duplicate": duplicate_issues_serialized + + duplicate_issues_related_serialized, + "relates_to": relates_to_issues_serialized + + relates_to_issues_related_serialized, } return Response(response_data, status=status.HTTP_200_OK) - def create(self, request, slug, project_id, issue_id): relation_type = request.data.get("relation_type", None) issues = request.data.get("issues", []) @@ -1431,9 +1531,15 @@ class IssueRelationViewSet(BaseViewSet): issue_relation = IssueRelation.objects.bulk_create( [ IssueRelation( - issue_id=issue if relation_type == "blocking" else issue_id, - related_issue_id=issue_id if relation_type == "blocking" else issue, - relation_type="blocked_by" if relation_type == "blocking" else relation_type, + issue_id=issue + if relation_type == "blocking" + else issue_id, + related_issue_id=issue_id + if relation_type == "blocking" + else issue, + relation_type="blocked_by" + if relation_type == "blocking" + else relation_type, project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, @@ -1472,11 +1578,17 @@ class IssueRelationViewSet(BaseViewSet): if relation_type == "blocking": issue_relation = IssueRelation.objects.get( - workspace__slug=slug, project_id=project_id, issue_id=related_issue, related_issue_id=issue_id + workspace__slug=slug, + project_id=project_id, + issue_id=related_issue, + related_issue_id=issue_id, ) else: issue_relation = IssueRelation.objects.get( - workspace__slug=slug, project_id=project_id, issue_id=issue_id, related_issue_id=related_issue + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + related_issue_id=related_issue, ) current_instance = json.dumps( IssueRelationSerializer(issue_relation).data, @@ -1505,7 +1617,9 @@ class IssueDraftViewSet(BaseViewSet): def get_queryset(self): return ( Issue.objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -1530,11 +1644,21 @@ class IssueDraftViewSet(BaseViewSet): @method_decorator(gzip_page) def list(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] order_by_param = request.GET.get("order_by", "-created_at") @@ -1550,7 +1674,9 @@ class IssueDraftViewSet(BaseViewSet): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -1560,7 +1686,9 @@ class IssueDraftViewSet(BaseViewSet): # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( - priority_order if order_by_param == "priority" else priority_order[::-1] + priority_order + if order_by_param == "priority" + else priority_order[::-1] ) issue_queryset = issue_queryset.annotate( priority_order=Case( @@ -1608,12 +1736,14 @@ class IssueDraftViewSet(BaseViewSet): else order_by_param ) ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + "-max_values" + if order_by_param.startswith("-") + else "max_values" ) else: issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueLiteSerializer( + issues = IssueSerializer( issue_queryset, many=True, fields=fields if fields else None ).data return Response(issues, status=status.HTTP_200_OK) @@ -1636,7 +1766,9 @@ class IssueDraftViewSet(BaseViewSet): # Track the issue issue_activity.delay( type="issue_draft.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), actor_id=str(request.user.id), issue_id=str(serializer.data.get("id", None)), project_id=str(project_id), @@ -1647,14 +1779,18 @@ class IssueDraftViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def partial_update(self, request, slug, project_id, pk): - issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) serializer = IssueSerializer(issue, data=request.data, partial=True) if serializer.is_valid(): - if request.data.get("is_draft") is not None and not request.data.get( + if request.data.get( "is_draft" - ): - serializer.save(created_at=timezone.now(), updated_at=timezone.now()) + ) is not None and not request.data.get("is_draft"): + serializer.save( + created_at=timezone.now(), updated_at=timezone.now() + ) else: serializer.save() issue_activity.delay( @@ -1679,7 +1815,9 @@ class IssueDraftViewSet(BaseViewSet): return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, pk=None): - issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) current_instance = json.dumps( IssueSerializer(issue).data, cls=DjangoJSONEncoder ) diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py index 6baf23121..09d763ab7 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -20,10 +20,13 @@ from plane.app.serializers import ( ModuleIssueSerializer, ModuleLinkSerializer, ModuleFavoriteSerializer, - IssueStateSerializer, + IssueSerializer, ModuleUserPropertiesSerializer, ) -from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission +from plane.app.permissions import ( + ProjectEntityPermission, + ProjectLitePermission, +) from plane.db.models import ( Module, ModuleIssue, @@ -33,6 +36,7 @@ from plane.db.models import ( ModuleFavorite, IssueLink, IssueAttachment, + IssueSubscriber, ModuleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity @@ -75,7 +79,9 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): .prefetch_related( Prefetch( "link_module", - queryset=ModuleLink.objects.select_related("module", "created_by"), + queryset=ModuleLink.objects.select_related( + "module", "created_by" + ), ) ) .annotate( @@ -156,7 +162,11 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): def list(self, request, slug, project_id): queryset = self.get_queryset() - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] modules = ModuleSerializer( queryset, many=True, fields=fields if fields else None ).data @@ -176,7 +186,13 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): .annotate(assignee_id=F("assignees__id")) .annotate(display_name=F("assignees__display_name")) .annotate(avatar=F("assignees__avatar")) - .values("first_name", "last_name", "assignee_id", "avatar", "display_name") + .values( + "first_name", + "last_name", + "assignee_id", + "avatar", + "display_name", + ) .annotate( total_issues=Count( "assignee_id", @@ -260,7 +276,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): if queryset.start_date and queryset.target_date: data["distribution"]["completion_chart"] = burndown_plot( - queryset=queryset, slug=slug, project_id=project_id, module_id=pk + queryset=queryset, + slug=slug, + project_id=project_id, + module_id=pk, ) return Response( @@ -269,9 +288,13 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): ) def destroy(self, request, slug, project_id, pk): - module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + module = Module.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) module_issues = list( - ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True) + ModuleIssue.objects.filter(module_id=pk).values_list( + "issue", flat=True + ) ) issue_activity.delay( type="module.activity.deleted", @@ -312,7 +335,9 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): super() .get_queryset() .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("issue") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -332,13 +357,19 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): @method_decorator(gzip_page) def list(self, request, slug, project_id, module_id): - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] order_by = request.GET.get("order_by", "created_at") filters = issue_filters(request.query_params, "GET") issues = ( Issue.issue_objects.filter(issue_module__module_id=module_id) .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -353,6 +384,8 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): .prefetch_related("labels") .order_by(order_by) .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) .order_by() @@ -360,13 +393,22 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + subscriber=self.request.user, issue_id=OuterRef("id") + ) + ) + ) ) - serializer = IssueStateSerializer( + serializer = IssueSerializer( issues, many=True, fields=fields if fields else None ) return Response(serializer.data, status=status.HTTP_200_OK) @@ -375,7 +417,8 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): issues = request.data.get("issues", []) if not len(issues): return Response( - {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, ) module = Module.objects.get( workspace__slug=slug, project_id=project_id, pk=module_id @@ -447,8 +490,12 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): epoch=int(timezone.now().timestamp()), ) + issues = self.get_queryset().values_list("issue_id", flat=True) + return Response( - ModuleIssueSerializer(self.get_queryset(), many=True).data, + IssueSerializer( + Issue.objects.filter(pk__in=issues), many=True + ).data, status=status.HTTP_200_OK, ) diff --git a/apiserver/plane/app/views/notification.py b/apiserver/plane/app/views/notification.py index 9494ea86c..15eef9cf0 100644 --- a/apiserver/plane/app/views/notification.py +++ b/apiserver/plane/app/views/notification.py @@ -51,8 +51,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator): # Filters based on query parameters snoozed_filters = { - "true": Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False), - "false": Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + "true": Q(snoozed_till__lt=timezone.now()) + | Q(snoozed_till__isnull=False), + "false": Q(snoozed_till__gte=timezone.now()) + | Q(snoozed_till__isnull=True), } notifications = notifications.filter(snoozed_filters[snoozed]) @@ -72,14 +74,18 @@ class NotificationViewSet(BaseViewSet, BasePaginator): issue_ids = IssueSubscriber.objects.filter( workspace__slug=slug, subscriber_id=request.user.id ).values_list("issue_id", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) # Assigned Issues if type == "assigned": issue_ids = IssueAssignee.objects.filter( workspace__slug=slug, assignee_id=request.user.id ).values_list("issue_id", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) # Created issues if type == "created": @@ -94,10 +100,14 @@ class NotificationViewSet(BaseViewSet, BasePaginator): issue_ids = Issue.objects.filter( workspace__slug=slug, created_by=request.user ).values_list("pk", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) # Pagination - if request.GET.get("per_page", False) and request.GET.get("cursor", False): + if request.GET.get("per_page", False) and request.GET.get( + "cursor", False + ): return self.paginate( request=request, queryset=(notifications), @@ -227,11 +237,13 @@ class MarkAllReadNotificationViewSet(BaseViewSet): # Filter for snoozed notifications if snoozed: notifications = notifications.filter( - Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False) + Q(snoozed_till__lt=timezone.now()) + | Q(snoozed_till__isnull=False) ) else: notifications = notifications.filter( - Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + Q(snoozed_till__gte=timezone.now()) + | Q(snoozed_till__isnull=True), ) # Filter for archived or unarchive @@ -245,14 +257,18 @@ class MarkAllReadNotificationViewSet(BaseViewSet): issue_ids = IssueSubscriber.objects.filter( workspace__slug=slug, subscriber_id=request.user.id ).values_list("issue_id", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) # Assigned Issues if type == "assigned": issue_ids = IssueAssignee.objects.filter( workspace__slug=slug, assignee_id=request.user.id ).values_list("issue_id", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) # Created issues if type == "created": @@ -267,7 +283,9 @@ class MarkAllReadNotificationViewSet(BaseViewSet): issue_ids = Issue.objects.filter( workspace__slug=slug, created_by=request.user ).values_list("pk", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) updated_notifications = [] for notification in notifications: diff --git a/apiserver/plane/app/views/page.py b/apiserver/plane/app/views/page.py index 482bdfbfe..1054b6af3 100644 --- a/apiserver/plane/app/views/page.py +++ b/apiserver/plane/app/views/page.py @@ -97,7 +97,9 @@ class PageViewSet(BaseViewSet): def partial_update(self, request, slug, project_id, pk): try: - page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) + page = Page.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id + ) if page.is_locked: return Response( @@ -127,7 +129,9 @@ class PageViewSet(BaseViewSet): 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) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) except Page.DoesNotExist: return Response( { @@ -161,12 +165,17 @@ class PageViewSet(BaseViewSet): return Response(pages, status=status.HTTP_200_OK) def archive(self, request, slug, project_id, page_id): - page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id) + page = Page.objects.get( + pk=page_id, workspace__slug=slug, project_id=project_id + ) # only the owner and admin can archive the page if ( ProjectMember.objects.filter( - project_id=project_id, member=request.user, is_active=True, role__gt=20 + project_id=project_id, + member=request.user, + is_active=True, + role__gt=20, ).exists() or request.user.id != page.owned_by_id ): @@ -180,12 +189,17 @@ class PageViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) def unarchive(self, request, slug, project_id, page_id): - page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id) + page = Page.objects.get( + pk=page_id, workspace__slug=slug, project_id=project_id + ) # only the owner and admin can un archive the page if ( ProjectMember.objects.filter( - project_id=project_id, member=request.user, is_active=True, role__gt=20 + project_id=project_id, + member=request.user, + is_active=True, + role__gt=20, ).exists() or request.user.id != page.owned_by_id ): @@ -212,14 +226,18 @@ class PageViewSet(BaseViewSet): pages = PageSerializer(pages, many=True).data return Response(pages, status=status.HTTP_200_OK) - def destroy(self, request, slug, project_id, pk): - page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) + page = Page.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id + ) # only the owner and admin can delete the page if ( ProjectMember.objects.filter( - project_id=project_id, member=request.user, is_active=True, role__gt=20 + project_id=project_id, + member=request.user, + is_active=True, + role__gt=20, ).exists() or request.user.id != page.owned_by_id ): diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index c5caac666..2895661f8 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -86,9 +86,15 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): super() .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) - .filter(Q(project_projectmember__member=self.request.user) | Q(network=2)) + .filter( + Q(project_projectmember__member=self.request.user) + | Q(network=2) + ) .select_related( - "workspace", "workspace__owner", "default_assignee", "project_lead" + "workspace", + "workspace__owner", + "default_assignee", + "project_lead", ) .annotate( is_favorite=Exists( @@ -160,7 +166,11 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): ) def list(self, request, slug): - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] sort_order_query = ProjectMember.objects.filter( member=request.user, @@ -173,7 +183,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): .annotate(sort_order=Subquery(sort_order_query)) .order_by("sort_order", "name") ) - if request.GET.get("per_page", False) and request.GET.get("cursor", False): + if request.GET.get("per_page", False) and request.GET.get( + "cursor", False + ): return self.paginate( request=request, queryset=(projects), @@ -181,10 +193,11 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): projects, many=True ).data, ) - projects = ProjectListSerializer(projects, many=True, fields=fields if fields else None).data + projects = ProjectListSerializer( + projects, many=True, fields=fields if fields else None + ).data return Response(projects, status=status.HTTP_200_OK) - def create(self, request, slug): try: workspace = Workspace.objects.get(slug=slug) @@ -197,7 +210,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): # Add the user as Administrator to the project project_member = ProjectMember.objects.create( - project_id=serializer.data["id"], member=request.user, role=20 + project_id=serializer.data["id"], + member=request.user, + role=20, ) # Also create the issue property for the user _ = IssueProperty.objects.create( @@ -270,9 +285,15 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): ] ) - project = self.get_queryset().filter(pk=serializer.data["id"]).first() + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) serializer = ProjectListSerializer(project) - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST, @@ -285,7 +306,8 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): ) except Workspace.DoesNotExist as e: return Response( - {"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Workspace does not exist"}, + status=status.HTTP_404_NOT_FOUND, ) except serializers.ValidationError as e: return Response( @@ -310,7 +332,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): serializer.save() if serializer.data["inbox_view"]: Inbox.objects.get_or_create( - name=f"{project.name} Inbox", project=project, is_default=True + name=f"{project.name} Inbox", + project=project, + is_default=True, ) # Create the triage state in Backlog group @@ -322,10 +346,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): color="#ff7700", ) - project = self.get_queryset().filter(pk=serializer.data["id"]).first() + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) serializer = ProjectListSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) except IntegrityError as e: if "already exists" in str(e): @@ -335,7 +365,8 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): ) except (Project.DoesNotExist, Workspace.DoesNotExist): return Response( - {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Project does not exist"}, + status=status.HTTP_404_NOT_FOUND, ) except serializers.ValidationError as e: return Response( @@ -370,11 +401,14 @@ class ProjectInvitationsViewset(BaseViewSet): # Check if email is provided if not emails: return Response( - {"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Emails are required"}, + status=status.HTTP_400_BAD_REQUEST, ) requesting_user = ProjectMember.objects.get( - workspace__slug=slug, project_id=project_id, member_id=request.user.id + workspace__slug=slug, + project_id=project_id, + member_id=request.user.id, ) # Check if any invited user has an higher role @@ -548,7 +582,9 @@ class ProjectJoinEndpoint(BaseAPIView): _ = WorkspaceMember.objects.create( workspace_id=project_invite.workspace_id, member=user, - role=15 if project_invite.role >= 15 else project_invite.role, + role=15 + if project_invite.role >= 15 + else project_invite.role, ) else: # Else make him active @@ -658,7 +694,8 @@ class ProjectMemberViewSet(BaseViewSet): sort_order = [ project_member.get("sort_order") for project_member in project_members - if str(project_member.get("member_id")) == str(member.get("member_id")) + if str(project_member.get("member_id")) + == str(member.get("member_id")) ] bulk_project_members.append( ProjectMember( @@ -666,7 +703,9 @@ class ProjectMemberViewSet(BaseViewSet): role=member.get("role", 10), project_id=project_id, workspace_id=project.workspace_id, - sort_order=sort_order[0] - 10000 if len(sort_order) else 65535, + sort_order=sort_order[0] - 10000 + if len(sort_order) + else 65535, ) ) bulk_issue_props.append( @@ -719,7 +758,9 @@ class ProjectMemberViewSet(BaseViewSet): is_active=True, ).select_related("project", "member", "workspace") - serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True) + serializer = ProjectMemberRoleSerializer( + project_members, fields=("id", "member", "role"), many=True + ) return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, slug, project_id, pk): @@ -747,7 +788,9 @@ class ProjectMemberViewSet(BaseViewSet): > requested_project_member.role ): return Response( - {"error": "You cannot update a role that is higher than your own role"}, + { + "error": "You cannot update a role that is higher than your own role" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -786,7 +829,9 @@ class ProjectMemberViewSet(BaseViewSet): # User cannot deactivate higher role if requesting_project_member.role < project_member.role: return Response( - {"error": "You cannot remove a user having role higher than you"}, + { + "error": "You cannot remove a user having role higher than you" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -837,7 +882,8 @@ class AddTeamToProjectEndpoint(BaseAPIView): if len(team_members) == 0: return Response( - {"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST + {"error": "No such team exists"}, + status=status.HTTP_400_BAD_REQUEST, ) workspace = Workspace.objects.get(slug=slug) @@ -884,7 +930,8 @@ class ProjectIdentifierEndpoint(BaseAPIView): if name == "": return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, ) exists = ProjectIdentifier.objects.filter( @@ -901,16 +948,23 @@ class ProjectIdentifierEndpoint(BaseAPIView): if name == "": return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST - ) - - if Project.objects.filter(identifier=name, workspace__slug=slug).exists(): - return Response( - {"error": "Cannot delete an identifier of an existing project"}, + {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST, ) - ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete() + if Project.objects.filter( + identifier=name, workspace__slug=slug + ).exists(): + return Response( + { + "error": "Cannot delete an identifier of an existing project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug + ).delete() return Response( status=status.HTTP_204_NO_CONTENT, @@ -928,7 +982,9 @@ class ProjectUserViewsEndpoint(BaseAPIView): ).first() if project_member is None: - return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) + return Response( + {"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN + ) view_props = project_member.view_props default_props = project_member.default_props @@ -936,8 +992,12 @@ class ProjectUserViewsEndpoint(BaseAPIView): sort_order = project_member.sort_order project_member.view_props = request.data.get("view_props", view_props) - project_member.default_props = request.data.get("default_props", default_props) - project_member.preferences = request.data.get("preferences", preferences) + project_member.default_props = request.data.get( + "default_props", default_props + ) + project_member.preferences = request.data.get( + "preferences", preferences + ) project_member.sort_order = request.data.get("sort_order", sort_order) project_member.save() @@ -1085,6 +1145,7 @@ class UserProjectRolesEndpoint(BaseAPIView): ).values("project_id", "role") project_members = { - str(member["project_id"]): member["role"] for member in project_members + str(member["project_id"]): member["role"] + for member in project_members } return Response(project_members, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/search.py b/apiserver/plane/app/views/search.py index 4ecb71127..0455541c6 100644 --- a/apiserver/plane/app/views/search.py +++ b/apiserver/plane/app/views/search.py @@ -10,7 +10,15 @@ from rest_framework.response import Response # Module imports from .base import BaseAPIView -from plane.db.models import Workspace, Project, Issue, Cycle, Module, Page, IssueView +from plane.db.models import ( + Workspace, + Project, + Issue, + Cycle, + Module, + Page, + IssueView, +) from plane.utils.issue_search import search_issues @@ -25,7 +33,9 @@ class GlobalSearchEndpoint(BaseAPIView): for field in fields: q |= Q(**{f"{field}__icontains": query}) return ( - Workspace.objects.filter(q, workspace_member__member=self.request.user) + Workspace.objects.filter( + q, workspace_member__member=self.request.user + ) .distinct() .values("name", "id", "slug") ) @@ -38,7 +48,8 @@ class GlobalSearchEndpoint(BaseAPIView): return ( Project.objects.filter( q, - Q(project_projectmember__member=self.request.user) | Q(network=2), + Q(project_projectmember__member=self.request.user) + | Q(network=2), workspace__slug=slug, ) .distinct() @@ -169,7 +180,9 @@ class GlobalSearchEndpoint(BaseAPIView): def get(self, request, slug): query = request.query_params.get("search", False) - workspace_search = request.query_params.get("workspace_search", "false") + workspace_search = request.query_params.get( + "workspace_search", "false" + ) project_id = request.query_params.get("project_id", False) if not query: @@ -209,7 +222,9 @@ class GlobalSearchEndpoint(BaseAPIView): class IssueSearchEndpoint(BaseAPIView): def get(self, request, slug, project_id): query = request.query_params.get("search", False) - workspace_search = request.query_params.get("workspace_search", "false") + workspace_search = request.query_params.get( + "workspace_search", "false" + ) parent = request.query_params.get("parent", "false") issue_relation = request.query_params.get("issue_relation", "false") cycle = request.query_params.get("cycle", "false") @@ -234,9 +249,9 @@ class IssueSearchEndpoint(BaseAPIView): issues = issues.filter( ~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True ).exclude( - pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list( - "parent_id", flat=True - ) + pk__in=Issue.issue_objects.filter( + parent__isnull=False + ).values_list("parent_id", flat=True) ) if issue_relation == "true" and issue_id: issue = Issue.issue_objects.get(pk=issue_id) diff --git a/apiserver/plane/app/views/state.py b/apiserver/plane/app/views/state.py index f7226ba6e..9c83cf006 100644 --- a/apiserver/plane/app/views/state.py +++ b/apiserver/plane/app/views/state.py @@ -77,16 +77,21 @@ class StateViewSet(BaseViewSet): ) if state.default: - return Response({"error": "Default state cannot be deleted"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": "Default state cannot be deleted"}, + status=status.HTTP_400_BAD_REQUEST, + ) # Check for any issues in the state issue_exist = Issue.issue_objects.filter(state=pk).exists() if issue_exist: return Response( - {"error": "The state is not empty, only empty states can be deleted"}, + { + "error": "The state is not empty, only empty states can be deleted" + }, status=status.HTTP_400_BAD_REQUEST, ) state.delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/user.py b/apiserver/plane/app/views/user.py index 008780526..7764e3b97 100644 --- a/apiserver/plane/app/views/user.py +++ b/apiserver/plane/app/views/user.py @@ -43,7 +43,9 @@ class UserEndpoint(BaseViewSet): is_admin = InstanceAdmin.objects.filter( instance=instance, user=request.user ).exists() - return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK) + return Response( + {"is_instance_admin": is_admin}, status=status.HTTP_200_OK + ) def deactivate(self, request): # Check all workspace user is active @@ -51,7 +53,12 @@ class UserEndpoint(BaseViewSet): # Instance admin check if InstanceAdmin.objects.filter(user=user).exists(): - return Response({"error": "You cannot deactivate your account since you are an instance admin"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + { + "error": "You cannot deactivate your account since you are an instance admin" + }, + status=status.HTTP_400_BAD_REQUEST, + ) projects_to_deactivate = [] workspaces_to_deactivate = [] @@ -61,7 +68,10 @@ class UserEndpoint(BaseViewSet): ).annotate( other_admin_exists=Count( Case( - When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1), + When( + Q(role=20, is_active=True) & ~Q(member=request.user), + then=1, + ), default=0, output_field=IntegerField(), ) @@ -86,7 +96,10 @@ class UserEndpoint(BaseViewSet): ).annotate( other_admin_exists=Count( Case( - When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1), + When( + Q(role=20, is_active=True) & ~Q(member=request.user), + then=1, + ), default=0, output_field=IntegerField(), ) @@ -95,7 +108,9 @@ class UserEndpoint(BaseViewSet): ) for workspace in workspaces: - if workspace.other_admin_exists > 0 or (workspace.total_members == 1): + if workspace.other_admin_exists > 0 or ( + workspace.total_members == 1 + ): workspace.is_active = False workspaces_to_deactivate.append(workspace) else: @@ -134,7 +149,9 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView): user = User.objects.get(pk=request.user.id, is_active=True) user.is_onboarded = request.data.get("is_onboarded", False) user.save() - return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK) + return Response( + {"message": "Updated successfully"}, status=status.HTTP_200_OK + ) class UpdateUserTourCompletedEndpoint(BaseAPIView): @@ -142,14 +159,16 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView): user = User.objects.get(pk=request.user.id, is_active=True) user.is_tour_completed = request.data.get("is_tour_completed", False) user.save() - return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK) + return Response( + {"message": "Updated successfully"}, status=status.HTTP_200_OK + ) class UserActivityEndpoint(BaseAPIView, BasePaginator): def get(self, request): - queryset = IssueActivity.objects.filter(actor=request.user).select_related( - "actor", "workspace", "issue", "project" - ) + queryset = IssueActivity.objects.filter( + actor=request.user + ).select_related("actor", "workspace", "issue", "project") return self.paginate( request=request, @@ -158,4 +177,3 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator): issue_activities, many=True ).data, ) - diff --git a/apiserver/plane/app/views/view.py b/apiserver/plane/app/views/view.py index a2f00a819..07bf1ad03 100644 --- a/apiserver/plane/app/views/view.py +++ b/apiserver/plane/app/views/view.py @@ -24,7 +24,7 @@ from . import BaseViewSet, BaseAPIView from plane.app.serializers import ( GlobalViewSerializer, IssueViewSerializer, - IssueLiteSerializer, + IssueSerializer, IssueViewFavoriteSerializer, ) from plane.app.permissions import ( @@ -42,6 +42,7 @@ from plane.db.models import ( IssueReaction, IssueLink, IssueAttachment, + IssueSubscriber, ) from plane.utils.issue_filters import issue_filters from plane.utils.grouper import group_results @@ -78,7 +79,9 @@ class GlobalViewIssuesViewSet(BaseViewSet): def get_queryset(self): return ( Issue.issue_objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -101,11 +104,21 @@ class GlobalViewIssuesViewSet(BaseViewSet): @method_decorator(gzip_page) def list(self, request, slug): filters = issue_filters(request.query_params, "GET") - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] order_by_param = request.GET.get("order_by", "-created_at") @@ -122,17 +135,36 @@ class GlobalViewIssuesViewSet(BaseViewSet): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + subscriber=self.request.user, issue_id=OuterRef("id") + ) + ) + ) ) # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( - priority_order if order_by_param == "priority" else priority_order[::-1] + priority_order + if order_by_param == "priority" + else priority_order[::-1] ) issue_queryset = issue_queryset.annotate( priority_order=Case( @@ -180,12 +212,14 @@ class GlobalViewIssuesViewSet(BaseViewSet): else order_by_param ) ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + "-max_values" + if order_by_param.startswith("-") + else "max_values" ) else: issue_queryset = issue_queryset.order_by(order_by_param) - serializer = IssueLiteSerializer( + serializer = IssueSerializer( issue_queryset, many=True, fields=fields if fields else None ) return Response(serializer.data, status=status.HTTP_200_OK) @@ -223,7 +257,11 @@ class IssueViewViewSet(BaseViewSet): def list(self, request, slug, project_id): queryset = self.get_queryset() - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] views = IssueViewSerializer( queryset, many=True, fields=fields if fields else None ).data diff --git a/apiserver/plane/app/views/webhook.py b/apiserver/plane/app/views/webhook.py index 48608d583..fe69cd7e6 100644 --- a/apiserver/plane/app/views/webhook.py +++ b/apiserver/plane/app/views/webhook.py @@ -26,8 +26,12 @@ class WebhookEndpoint(BaseAPIView): ) 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) + 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( diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index f51e1ac1e..9cedff8f4 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -66,7 +66,7 @@ from plane.db.models import ( WorkspaceMember, CycleIssue, IssueReaction, - WorkspaceUserProperties + WorkspaceUserProperties, ) from plane.app.permissions import ( WorkSpaceBasePermission, @@ -116,7 +116,9 @@ class WorkSpaceViewSet(BaseViewSet): .values("count") ) return ( - self.filter_queryset(super().get_queryset().select_related("owner")) + self.filter_queryset( + super().get_queryset().select_related("owner") + ) .order_by("name") .filter( workspace_member__member=self.request.user, @@ -142,7 +144,9 @@ class WorkSpaceViewSet(BaseViewSet): if len(name) > 80 or len(slug) > 48: return Response( - {"error": "The maximum length for name is 80 and for slug is 48"}, + { + "error": "The maximum length for name is 80 and for slug is 48" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -155,7 +159,9 @@ class WorkSpaceViewSet(BaseViewSet): role=20, company_role=request.data.get("company_role", ""), ) - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) return Response( [serializer.errors[error][0] for error in serializer.errors], status=status.HTTP_400_BAD_REQUEST, @@ -178,7 +184,11 @@ class UserWorkSpacesEndpoint(BaseAPIView): ] def get(self, request): - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] member_count = ( WorkspaceMember.objects.filter( workspace=OuterRef("id"), @@ -210,7 +220,8 @@ class UserWorkSpacesEndpoint(BaseAPIView): .annotate(total_members=member_count) .annotate(total_issues=issue_count) .filter( - workspace_member__member=request.user, workspace_member__is_active=True + workspace_member__member=request.user, + workspace_member__is_active=True, ) .distinct() ) @@ -259,7 +270,8 @@ class WorkspaceInvitationsViewset(BaseViewSet): # Check if email is provided if not emails: return Response( - {"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Emails are required"}, + status=status.HTTP_400_BAD_REQUEST, ) # check for role level of the requesting user @@ -586,7 +598,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): > requested_workspace_member.role ): return Response( - {"error": "You cannot update a role that is higher than your own role"}, + { + "error": "You cannot update a role that is higher than your own role" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -625,7 +639,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): if requesting_workspace_member.role < workspace_member.role: return Response( - {"error": "You cannot remove a user having role higher than you"}, + { + "error": "You cannot remove a user having role higher than you" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -729,11 +745,15 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView): def get(self, request, slug): # Fetch all project IDs where the user is involved - project_ids = ProjectMember.objects.filter( - member=request.user, - member__is_bot=False, - is_active=True, - ).values_list('project_id', flat=True).distinct() + project_ids = ( + ProjectMember.objects.filter( + member=request.user, + member__is_bot=False, + is_active=True, + ) + .values_list("project_id", flat=True) + .distinct() + ) # Get all the project members in which the user is involved project_members = ProjectMember.objects.filter( @@ -742,7 +762,9 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView): project_id__in=project_ids, is_active=True, ).select_related("project", "member", "workspace") - project_members = ProjectMemberRoleSerializer(project_members, many=True).data + project_members = ProjectMemberRoleSerializer( + project_members, many=True + ).data project_members_dict = dict() @@ -790,7 +812,9 @@ class TeamMemberViewSet(BaseViewSet): ) if len(members) != len(request.data.get("members", [])): - users = list(set(request.data.get("members", [])).difference(members)) + users = list( + set(request.data.get("members", [])).difference(members) + ) users = User.objects.filter(pk__in=users) serializer = UserLiteSerializer(users, many=True) @@ -804,7 +828,9 @@ class TeamMemberViewSet(BaseViewSet): workspace = Workspace.objects.get(slug=slug) - serializer = TeamSerializer(data=request.data, context={"workspace": workspace}) + serializer = TeamSerializer( + data=request.data, context={"workspace": workspace} + ) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -833,7 +859,9 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): workspace_id=last_workspace_id, member=request.user ).select_related("workspace", "project", "member", "workspace__owner") - project_member_serializer = ProjectMemberSerializer(project_member, many=True) + project_member_serializer = ProjectMemberSerializer( + project_member, many=True + ) return Response( { @@ -1017,7 +1045,11 @@ class WorkspaceThemeViewSet(BaseViewSet): serializer_class = WorkspaceThemeSerializer def get_queryset(self): - return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug")) + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + ) def create(self, request, slug): workspace = Workspace.objects.get(slug=slug) @@ -1280,12 +1312,22 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): ] def get(self, request, slug, user_id): - fields = [field for field in request.GET.get("fields", "").split(",") if field] + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] filters = issue_filters(request.query_params, "GET") # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] order_by_param = request.GET.get("order_by", "-created_at") issue_queryset = ( @@ -1298,7 +1340,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): ) .filter(**filters) .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -1319,7 +1363,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -1329,7 +1375,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( - priority_order if order_by_param == "priority" else priority_order[::-1] + priority_order + if order_by_param == "priority" + else priority_order[::-1] ) issue_queryset = issue_queryset.annotate( priority_order=Case( @@ -1377,7 +1425,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): else order_by_param ) ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + "-max_values" + if order_by_param.startswith("-") + else "max_values" ) else: issue_queryset = issue_queryset.order_by(order_by_param) @@ -1397,7 +1447,9 @@ class WorkspaceLabelsEndpoint(BaseAPIView): labels = Label.objects.filter( workspace__slug=slug, project__project_projectmember__member=request.user, - ).values("parent", "name", "color", "id", "project_id", "workspace__slug") + ).values( + "parent", "name", "color", "id", "project_id", "workspace__slug" + ) return Response(labels, status=status.HTTP_200_OK) @@ -1411,18 +1463,27 @@ class WorkspaceUserPropertiesEndpoint(BaseAPIView): user=request.user, workspace__slug=slug, ) - - workspace_properties.filters = request.data.get("filters", workspace_properties.filters) - workspace_properties.display_filters = request.data.get("display_filters", workspace_properties.display_filters) - workspace_properties.display_properties = request.data.get("display_properties", workspace_properties.display_properties) + + workspace_properties.filters = request.data.get( + "filters", workspace_properties.filters + ) + workspace_properties.display_filters = request.data.get( + "display_filters", workspace_properties.display_filters + ) + workspace_properties.display_properties = request.data.get( + "display_properties", workspace_properties.display_properties + ) workspace_properties.save() serializer = WorkspaceUserPropertiesSerializer(workspace_properties) return Response(serializer.data, status=status.HTTP_201_CREATED) def get(self, request, slug): - workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create( + ( + workspace_properties, + _, + ) = WorkspaceUserProperties.objects.get_or_create( user=request.user, workspace__slug=slug ) serializer = WorkspaceUserPropertiesSerializer(workspace_properties) - return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index a4f5b194c..778956229 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -101,7 +101,9 @@ def get_assignee_details(slug, filters): def get_label_details(slug, filters): """Fetch label details if required""" return ( - Issue.objects.filter(workspace__slug=slug, **filters, labels__id__isnull=False) + Issue.objects.filter( + workspace__slug=slug, **filters, labels__id__isnull=False + ) .distinct("labels__id") .order_by("labels__id") .values("labels__id", "labels__color", "labels__name") @@ -174,7 +176,9 @@ def generate_segmented_rows( ): segment_zero = list( set( - item.get("segment") for sublist in distribution.values() for item in sublist + item.get("segment") + for sublist in distribution.values() + for item in sublist ) ) @@ -193,7 +197,9 @@ def generate_segmented_rows( ] for segment in segment_zero: - value = next((x.get(key) for x in data if x.get("segment") == segment), "0") + value = next( + (x.get(key) for x in data if x.get("segment") == segment), "0" + ) generated_row.append(value) if x_axis == ASSIGNEE_ID: @@ -212,7 +218,11 @@ def generate_segmented_rows( if x_axis == LABEL_ID: label = next( - (lab for lab in label_details if str(lab[LABEL_ID]) == str(item)), + ( + lab + for lab in label_details + if str(lab[LABEL_ID]) == str(item) + ), None, ) @@ -221,7 +231,11 @@ def generate_segmented_rows( if x_axis == STATE_ID: state = next( - (sta for sta in state_details if str(sta[STATE_ID]) == str(item)), + ( + sta + for sta in state_details + if str(sta[STATE_ID]) == str(item) + ), None, ) @@ -230,7 +244,11 @@ def generate_segmented_rows( if x_axis == CYCLE_ID: cycle = next( - (cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)), + ( + cyc + for cyc in cycle_details + if str(cyc[CYCLE_ID]) == str(item) + ), None, ) @@ -239,7 +257,11 @@ def generate_segmented_rows( if x_axis == MODULE_ID: module = next( - (mod for mod in module_details if str(mod[MODULE_ID]) == str(item)), + ( + mod + for mod in module_details + if str(mod[MODULE_ID]) == str(item) + ), None, ) @@ -266,7 +288,11 @@ def generate_segmented_rows( if segmented == LABEL_ID: for index, segm in enumerate(row_zero[2:]): label = next( - (lab for lab in label_details if str(lab[LABEL_ID]) == str(segm)), + ( + lab + for lab in label_details + if str(lab[LABEL_ID]) == str(segm) + ), None, ) if label: @@ -275,7 +301,11 @@ def generate_segmented_rows( if segmented == STATE_ID: for index, segm in enumerate(row_zero[2:]): state = next( - (sta for sta in state_details if str(sta[STATE_ID]) == str(segm)), + ( + sta + for sta in state_details + if str(sta[STATE_ID]) == str(segm) + ), None, ) if state: @@ -284,7 +314,11 @@ def generate_segmented_rows( if segmented == MODULE_ID: for index, segm in enumerate(row_zero[2:]): module = next( - (mod for mod in label_details if str(mod[MODULE_ID]) == str(segm)), + ( + mod + for mod in label_details + if str(mod[MODULE_ID]) == str(segm) + ), None, ) if module: @@ -293,7 +327,11 @@ def generate_segmented_rows( if segmented == CYCLE_ID: for index, segm in enumerate(row_zero[2:]): cycle = next( - (cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(segm)), + ( + cyc + for cyc in cycle_details + if str(cyc[CYCLE_ID]) == str(segm) + ), None, ) if cycle: @@ -315,7 +353,10 @@ def generate_non_segmented_rows( ): rows = [] for item, data in distribution.items(): - row = [item, data[0].get("count" if y_axis == "issue_count" else "estimate")] + row = [ + item, + data[0].get("count" if y_axis == "issue_count" else "estimate"), + ] if x_axis == ASSIGNEE_ID: assignee = next( @@ -333,7 +374,11 @@ def generate_non_segmented_rows( if x_axis == LABEL_ID: label = next( - (lab for lab in label_details if str(lab[LABEL_ID]) == str(item)), + ( + lab + for lab in label_details + if str(lab[LABEL_ID]) == str(item) + ), None, ) @@ -342,7 +387,11 @@ def generate_non_segmented_rows( if x_axis == STATE_ID: state = next( - (sta for sta in state_details if str(sta[STATE_ID]) == str(item)), + ( + sta + for sta in state_details + if str(sta[STATE_ID]) == str(item) + ), None, ) @@ -351,7 +400,11 @@ def generate_non_segmented_rows( if x_axis == CYCLE_ID: cycle = next( - (cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)), + ( + cyc + for cyc in cycle_details + if str(cyc[CYCLE_ID]) == str(item) + ), None, ) @@ -360,7 +413,11 @@ def generate_non_segmented_rows( if x_axis == MODULE_ID: module = next( - (mod for mod in module_details if str(mod[MODULE_ID]) == str(item)), + ( + mod + for mod in module_details + if str(mod[MODULE_ID]) == str(item) + ), None, ) @@ -369,7 +426,10 @@ def generate_non_segmented_rows( rows.append(tuple(row)) - row_zero = [row_mapping.get(x_axis, "X-Axis"), row_mapping.get(y_axis, "Y-Axis")] + row_zero = [ + row_mapping.get(x_axis, "X-Axis"), + row_mapping.get(y_axis, "Y-Axis"), + ] return [tuple(row_zero)] + rows diff --git a/apiserver/plane/bgtasks/apps.py b/apiserver/plane/bgtasks/apps.py index 03d29f3e0..7f6ca38f0 100644 --- a/apiserver/plane/bgtasks/apps.py +++ b/apiserver/plane/bgtasks/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class BgtasksConfig(AppConfig): - name = 'plane.bgtasks' + name = "plane.bgtasks" diff --git a/apiserver/plane/bgtasks/event_tracking_task.py b/apiserver/plane/bgtasks/event_tracking_task.py index 7d26dd4ab..82a8281a9 100644 --- a/apiserver/plane/bgtasks/event_tracking_task.py +++ b/apiserver/plane/bgtasks/event_tracking_task.py @@ -40,22 +40,24 @@ def auth_events(user, email, user_agent, ip, event_name, medium, first_time): email, event=event_name, properties={ - "event_id": uuid.uuid4().hex, - "user": {"email": email, "id": str(user)}, - "device_ctx": { - "ip": ip, - "user_agent": user_agent, - }, - "medium": medium, - "first_time": first_time - } + "event_id": uuid.uuid4().hex, + "user": {"email": email, "id": str(user)}, + "device_ctx": { + "ip": ip, + "user_agent": user_agent, + }, + "medium": medium, + "first_time": first_time, + }, ) except Exception as e: capture_exception(e) - + @shared_task -def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from): +def workspace_invite_event( + user, email, user_agent, ip, event_name, accepted_from +): try: POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration() @@ -65,14 +67,14 @@ def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_fro email, event=event_name, properties={ - "event_id": uuid.uuid4().hex, - "user": {"email": email, "id": str(user)}, - "device_ctx": { - "ip": ip, - "user_agent": user_agent, - }, - "accepted_from": accepted_from - } + "event_id": uuid.uuid4().hex, + "user": {"email": email, "id": str(user)}, + "device_ctx": { + "ip": ip, + "user_agent": user_agent, + }, + "accepted_from": accepted_from, + }, ) except Exception as e: - capture_exception(e) \ No newline at end of file + capture_exception(e) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index e895b859d..f9e6c1ac8 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -68,7 +68,9 @@ def create_zip_file(files): def upload_to_s3(zip_file, workspace_id, token_id, slug): - file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip" + file_name = ( + f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip" + ) expires_in = 7 * 24 * 60 * 60 if settings.USE_MINIO: @@ -87,7 +89,10 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug): ) presigned_url = s3.generate_presigned_url( "get_object", - Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name}, + Params={ + "Bucket": settings.AWS_STORAGE_BUCKET_NAME, + "Key": file_name, + }, ExpiresIn=expires_in, ) # Create the new url with updated domain and protocol @@ -112,7 +117,10 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug): presigned_url = s3.generate_presigned_url( "get_object", - Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name}, + Params={ + "Bucket": settings.AWS_STORAGE_BUCKET_NAME, + "Key": file_name, + }, ExpiresIn=expires_in, ) @@ -172,11 +180,17 @@ def generate_json_row(issue): else "", "Labels": issue["labels__name"], "Cycle Name": issue["issue_cycle__cycle__name"], - "Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]), + "Cycle Start Date": dateConverter( + issue["issue_cycle__cycle__start_date"] + ), "Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]), "Module Name": issue["issue_module__module__name"], - "Module Start Date": dateConverter(issue["issue_module__module__start_date"]), - "Module Target Date": dateConverter(issue["issue_module__module__target_date"]), + "Module Start Date": dateConverter( + issue["issue_module__module__start_date"] + ), + "Module Target Date": dateConverter( + issue["issue_module__module__target_date"] + ), "Created At": dateTimeConverter(issue["created_at"]), "Updated At": dateTimeConverter(issue["updated_at"]), "Completed At": dateTimeConverter(issue["completed_at"]), @@ -211,7 +225,11 @@ def update_json_row(rows, row): def update_table_row(rows, row): matched_index = next( - (index for index, existing_row in enumerate(rows) if existing_row[0] == row[0]), + ( + index + for index, existing_row in enumerate(rows) + if existing_row[0] == row[0] + ), None, ) @@ -260,7 +278,9 @@ def generate_xlsx(header, project_id, issues, files): @shared_task -def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, slug): +def issue_export_task( + provider, workspace_id, project_ids, token_id, multiple, slug +): try: exporter_instance = ExporterHistory.objects.get(token=token_id) exporter_instance.status = "processing" @@ -273,9 +293,14 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s project_id__in=project_ids, project__project_projectmember__member=exporter_instance.initiated_by_id, ) - .select_related("project", "workspace", "state", "parent", "created_by") + .select_related( + "project", "workspace", "state", "parent", "created_by" + ) .prefetch_related( - "assignees", "labels", "issue_cycle__cycle", "issue_module__module" + "assignees", + "labels", + "issue_cycle__cycle", + "issue_module__module", ) .values( "id", diff --git a/apiserver/plane/bgtasks/exporter_expired_task.py b/apiserver/plane/bgtasks/exporter_expired_task.py index 30b638c84..d408c6476 100644 --- a/apiserver/plane/bgtasks/exporter_expired_task.py +++ b/apiserver/plane/bgtasks/exporter_expired_task.py @@ -19,7 +19,8 @@ from plane.db.models import ExporterHistory def delete_old_s3_link(): # Get a list of keys and IDs to process expired_exporter_history = ExporterHistory.objects.filter( - Q(url__isnull=False) & Q(created_at__lte=timezone.now() - timedelta(days=8)) + Q(url__isnull=False) + & Q(created_at__lte=timezone.now() - timedelta(days=8)) ).values_list("key", "id") if settings.USE_MINIO: s3 = boto3.client( @@ -42,8 +43,12 @@ def delete_old_s3_link(): # Delete object from S3 if file_name: if settings.USE_MINIO: - s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name) + s3.delete_object( + Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name + ) else: - s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name) + s3.delete_object( + Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name + ) ExporterHistory.objects.filter(id=exporter_id).update(url=None) diff --git a/apiserver/plane/bgtasks/file_asset_task.py b/apiserver/plane/bgtasks/file_asset_task.py index 339d24583..e372355ef 100644 --- a/apiserver/plane/bgtasks/file_asset_task.py +++ b/apiserver/plane/bgtasks/file_asset_task.py @@ -14,10 +14,10 @@ from plane.db.models import FileAsset @shared_task def delete_file_asset(): - # file assets to delete file_assets_to_delete = FileAsset.objects.filter( - Q(is_deleted=True) & Q(updated_at__lte=timezone.now() - timedelta(days=7)) + Q(is_deleted=True) + & Q(updated_at__lte=timezone.now() - timedelta(days=7)) ) # Delete the file from storage and the file object from the database @@ -26,4 +26,3 @@ def delete_file_asset(): file_asset.asset.delete(save=False) # Delete the file object file_asset.delete() - diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index d790f845d..6c966f342 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -42,7 +42,9 @@ def forgot_password(first_name, email, uidb64, token, current_site): "email": email, } - html_content = render_to_string("emails/auth/forgot_password.html", context) + html_content = render_to_string( + "emails/auth/forgot_password.html", context + ) text_content = strip_tags(html_content) diff --git a/apiserver/plane/bgtasks/importer_task.py b/apiserver/plane/bgtasks/importer_task.py index 84d10ecd3..f58085249 100644 --- a/apiserver/plane/bgtasks/importer_task.py +++ b/apiserver/plane/bgtasks/importer_task.py @@ -25,7 +25,6 @@ from plane.db.models import ( User, IssueProperty, ) -from plane.bgtasks.user_welcome_task import send_welcome_slack @shared_task @@ -55,15 +54,6 @@ def service_importer(service, importer_id): ignore_conflicts=True, ) - _ = [ - send_welcome_slack.delay( - str(user.id), - True, - f"{user.email} was imported to Plane from {service}", - ) - for user in new_users - ] - workspace_users = User.objects.filter( email__in=[ user.get("email").strip().lower() @@ -130,12 +120,17 @@ def service_importer(service, importer_id): repository_id = importer.metadata.get("repository_id", False) workspace_integration = WorkspaceIntegration.objects.get( - workspace_id=importer.workspace_id, integration__provider="github" + workspace_id=importer.workspace_id, + integration__provider="github", ) # Delete the old repository object - GithubRepositorySync.objects.filter(project_id=importer.project_id).delete() - GithubRepository.objects.filter(project_id=importer.project_id).delete() + GithubRepositorySync.objects.filter( + project_id=importer.project_id + ).delete() + GithubRepository.objects.filter( + project_id=importer.project_id + ).delete() # Create a Label for github label = Label.objects.filter( diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 2552ffbc5..686f06a20 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -138,8 +138,12 @@ def track_parent( project_id=project_id, workspace_id=workspace_id, comment=f"updated the parent issue to", - old_identifier=old_parent.id if old_parent is not None else None, - new_identifier=new_parent.id if new_parent is not None else None, + old_identifier=old_parent.id + if old_parent is not None + else None, + new_identifier=new_parent.id + if new_parent is not None + else None, epoch=epoch, ) ) @@ -217,7 +221,9 @@ def track_target_date( issue_activities, epoch, ): - if current_instance.get("target_date") != requested_data.get("target_date"): + if current_instance.get("target_date") != requested_data.get( + "target_date" + ): issue_activities.append( IssueActivity( issue_id=issue_id, @@ -281,8 +287,12 @@ def track_labels( issue_activities, epoch, ): - requested_labels = set([str(lab) for lab in requested_data.get("labels", [])]) - current_labels = set([str(lab) for lab in current_instance.get("labels", [])]) + requested_labels = set( + [str(lab) for lab in requested_data.get("labels", [])] + ) + current_labels = set( + [str(lab) for lab in current_instance.get("labels", [])] + ) added_labels = requested_labels - current_labels dropped_labels = current_labels - requested_labels @@ -339,8 +349,12 @@ def track_assignees( issue_activities, epoch, ): - requested_assignees = set([str(asg) for asg in requested_data.get("assignees", [])]) - current_assignees = set([str(asg) for asg in current_instance.get("assignees", [])]) + requested_assignees = set( + [str(asg) for asg in requested_data.get("assignees", [])] + ) + current_assignees = set( + [str(asg) for asg in current_instance.get("assignees", [])] + ) added_assignees = requested_assignees - current_assignees dropped_assginees = current_assignees - requested_assignees @@ -392,7 +406,9 @@ def track_estimate_points( issue_activities, epoch, ): - if current_instance.get("estimate_point") != requested_data.get("estimate_point"): + if current_instance.get("estimate_point") != requested_data.get( + "estimate_point" + ): issue_activities.append( IssueActivity( issue_id=issue_id, @@ -423,7 +439,9 @@ def track_archive_at( issue_activities, epoch, ): - if current_instance.get("archived_at") != requested_data.get("archived_at"): + if current_instance.get("archived_at") != requested_data.get( + "archived_at" + ): if requested_data.get("archived_at") is None: issue_activities.append( IssueActivity( @@ -536,7 +554,9 @@ def update_issue_activity( "closed_to": track_closed_to, } - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -589,7 +609,9 @@ def create_comment_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -621,12 +643,16 @@ def update_comment_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) - if current_instance.get("comment_html") != requested_data.get("comment_html"): + if current_instance.get("comment_html") != requested_data.get( + "comment_html" + ): issue_activities.append( IssueActivity( issue_id=issue_id, @@ -680,14 +706,18 @@ def create_cycle_issue_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) # Updated Records: updated_records = current_instance.get("updated_cycle_issues", []) - created_records = json.loads(current_instance.get("created_cycle_issues", [])) + created_records = json.loads( + current_instance.get("created_cycle_issues", []) + ) for updated_record in updated_records: old_cycle = Cycle.objects.filter( @@ -756,7 +786,9 @@ def delete_cycle_issue_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -798,14 +830,18 @@ def create_module_issue_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) # Updated Records: updated_records = current_instance.get("updated_module_issues", []) - created_records = json.loads(current_instance.get("created_module_issues", [])) + created_records = json.loads( + current_instance.get("created_module_issues", []) + ) for updated_record in updated_records: old_module = Module.objects.filter( @@ -873,7 +909,9 @@ def delete_module_issue_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -915,7 +953,9 @@ def create_link_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -946,7 +986,9 @@ def update_link_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -1010,7 +1052,9 @@ def create_attachment_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -1065,7 +1109,9 @@ def create_issue_reaction_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) if requested_data and requested_data.get("reaction") is not None: issue_reaction = ( IssueReaction.objects.filter( @@ -1137,7 +1183,9 @@ def create_comment_reaction_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) if requested_data and requested_data.get("reaction") is not None: comment_reaction_id, comment_id = ( CommentReaction.objects.filter( @@ -1148,7 +1196,9 @@ def create_comment_reaction_activity( .values_list("id", "comment__id") .first() ) - comment = IssueComment.objects.get(pk=comment_id, project_id=project_id) + comment = IssueComment.objects.get( + pk=comment_id, project_id=project_id + ) if ( comment is not None and comment_reaction_id is not None @@ -1222,7 +1272,9 @@ def create_issue_vote_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) if requested_data and requested_data.get("vote") is not None: issue_activities.append( IssueActivity( @@ -1284,12 +1336,14 @@ def create_issue_relation_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) if current_instance is None and requested_data.get("issues") is not None: - for related_issue in requested_data.get("issues"): + for related_issue in requested_data.get("issues"): issue = Issue.objects.get(pk=related_issue) issue_activities.append( IssueActivity( @@ -1339,7 +1393,9 @@ def delete_issue_relation_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -1382,6 +1438,7 @@ def delete_issue_relation_activity( ) ) + def create_draft_issue_activity( requested_data, current_instance, @@ -1416,7 +1473,9 @@ def update_draft_issue_activity( issue_activities, epoch, ): - requested_data = json.loads(requested_data) if requested_data is not None else None + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -1543,7 +1602,9 @@ def issue_activity( ) # Save all the values to database - issue_activities_created = IssueActivity.objects.bulk_create(issue_activities) + issue_activities_created = IssueActivity.objects.bulk_create( + issue_activities + ) # Post the updates to segway for integrations and webhooks if len(issue_activities_created): # Don't send activities if the actor is a bot @@ -1570,7 +1631,9 @@ def issue_activity( project_id=project_id, subscriber=subscriber, issue_activities_created=json.dumps( - IssueActivitySerializer(issue_activities_created, many=True).data, + IssueActivitySerializer( + issue_activities_created, many=True + ).data, cls=DjangoJSONEncoder, ), requested_data=requested_data, diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index 6a09b08ba..718b60355 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -36,7 +36,9 @@ def archive_old_issues(): Q( project=project_id, archived_at__isnull=True, - updated_at__lte=(timezone.now() - timedelta(days=archive_in * 30)), + updated_at__lte=( + timezone.now() - timedelta(days=archive_in * 30) + ), state__group__in=["completed", "cancelled"], ), Q(issue_cycle__isnull=True) @@ -46,7 +48,9 @@ def archive_old_issues(): ), Q(issue_module__isnull=True) | ( - Q(issue_module__module__target_date__lt=timezone.now().date()) + Q( + issue_module__module__target_date__lt=timezone.now().date() + ) & Q(issue_module__isnull=False) ), ).filter( @@ -74,7 +78,9 @@ def archive_old_issues(): _ = [ issue_activity.delay( type="issue.activity.updated", - requested_data=json.dumps({"archived_at": str(archive_at)}), + requested_data=json.dumps( + {"archived_at": str(archive_at)} + ), actor_id=str(project.created_by_id), issue_id=issue.id, project_id=project_id, @@ -108,7 +114,9 @@ def close_old_issues(): Q( project=project_id, archived_at__isnull=True, - updated_at__lte=(timezone.now() - timedelta(days=close_in * 30)), + updated_at__lte=( + timezone.now() - timedelta(days=close_in * 30) + ), state__group__in=["backlog", "unstarted", "started"], ), Q(issue_cycle__isnull=True) @@ -118,7 +126,9 @@ def close_old_issues(): ), Q(issue_module__isnull=True) | ( - Q(issue_module__module__target_date__lt=timezone.now().date()) + Q( + issue_module__module__target_date__lt=timezone.now().date() + ) & Q(issue_module__isnull=False) ), ).filter( @@ -131,7 +141,9 @@ def close_old_issues(): # Check if Issues if issues: if project.default_state is None: - close_state = State.objects.filter(group="cancelled").first() + close_state = State.objects.filter( + group="cancelled" + ).first() else: close_state = project.default_state @@ -165,4 +177,4 @@ def close_old_issues(): if settings.DEBUG: print(e) capture_exception(e) - return \ No newline at end of file + return diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index bb61e0ada..b94ec4bfe 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -33,7 +33,9 @@ def magic_link(email, key, token, current_site): subject = f"Your unique Plane login code is {token}" context = {"code": token, "email": email} - html_content = render_to_string("emails/auth/magic_signin.html", context) + html_content = render_to_string( + "emails/auth/magic_signin.html", context + ) text_content = strip_tags(html_content) connection = get_connection( diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index d33b883bb..5649ad6b7 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -12,7 +12,7 @@ from plane.db.models import ( Issue, Notification, IssueComment, - IssueActivity + IssueActivity, ) # Third Party imports @@ -20,9 +20,9 @@ from celery import shared_task from bs4 import BeautifulSoup - # =========== Issue Description Html Parsing and Notification Functions ====================== + def update_mentions_for_issue(issue, project, new_mentions, removed_mention): aggregated_issue_mentions = [] @@ -32,14 +32,14 @@ def update_mentions_for_issue(issue, project, new_mentions, removed_mention): mention_id=mention_id, issue=issue, project=project, - workspace_id=project.workspace_id + workspace_id=project.workspace_id, ) ) - IssueMention.objects.bulk_create( - aggregated_issue_mentions, batch_size=100) + IssueMention.objects.bulk_create(aggregated_issue_mentions, batch_size=100) IssueMention.objects.filter( - issue=issue, mention__in=removed_mention).delete() + issue=issue, mention__in=removed_mention + ).delete() def get_new_mentions(requested_instance, current_instance): @@ -48,15 +48,17 @@ def get_new_mentions(requested_instance, current_instance): # extract mentions from both the instance of data mentions_older = extract_mentions(current_instance) - + mentions_newer = extract_mentions(requested_instance) # Getting Set Difference from mentions_newer new_mentions = [ - mention for mention in mentions_newer if mention not in mentions_older] + mention for mention in mentions_newer if mention not in mentions_older + ] return new_mentions + # Get Removed Mention @@ -70,10 +72,12 @@ def get_removed_mentions(requested_instance, current_instance): # Getting Set Difference from mentions_newer removed_mentions = [ - mention for mention in mentions_older if mention not in mentions_newer] + mention for mention in mentions_older if mention not in mentions_newer + ] return removed_mentions + # Adds mentions as subscribers @@ -84,27 +88,34 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions): for mention_id in mentions: # If the particular mention has not already been subscribed to the issue, he must be sent the mentioned notification - if not IssueSubscriber.objects.filter( - issue_id=issue_id, - subscriber_id=mention_id, - project_id=project_id, - ).exists() and not IssueAssignee.objects.filter( - project_id=project_id, issue_id=issue_id, - assignee_id=mention_id - ).exists() and not Issue.objects.filter( - project_id=project_id, pk=issue_id, created_by_id=mention_id - ).exists(): - - project = Project.objects.get(pk=project_id) - - bulk_mention_subscribers.append(IssueSubscriber( - workspace_id=project.workspace_id, - project_id=project_id, + if ( + not IssueSubscriber.objects.filter( issue_id=issue_id, subscriber_id=mention_id, - )) + project_id=project_id, + ).exists() + and not IssueAssignee.objects.filter( + project_id=project_id, + issue_id=issue_id, + assignee_id=mention_id, + ).exists() + and not Issue.objects.filter( + project_id=project_id, pk=issue_id, created_by_id=mention_id + ).exists() + ): + project = Project.objects.get(pk=project_id) + + bulk_mention_subscribers.append( + IssueSubscriber( + workspace_id=project.workspace_id, + project_id=project_id, + issue_id=issue_id, + subscriber_id=mention_id, + ) + ) return bulk_mention_subscribers + # Parse Issue Description & extracts mentions def extract_mentions(issue_instance): try: @@ -113,46 +124,56 @@ def extract_mentions(issue_instance): # Convert string to dictionary data = json.loads(issue_instance) html = data.get("description_html") - soup = BeautifulSoup(html, 'html.parser') + soup = BeautifulSoup(html, "html.parser") mention_tags = soup.find_all( - 'mention-component', attrs={'target': 'users'}) + "mention-component", attrs={"target": "users"} + ) - mentions = [mention_tag['id'] for mention_tag in mention_tags] + mentions = [mention_tag["id"] for mention_tag in mention_tags] return list(set(mentions)) except Exception as e: return [] - - + + # =========== Comment Parsing and Notification Functions ====================== def extract_comment_mentions(comment_value): try: mentions = [] - soup = BeautifulSoup(comment_value, 'html.parser') + soup = BeautifulSoup(comment_value, "html.parser") mentions_tags = soup.find_all( - 'mention-component', attrs={'target': 'users'} + "mention-component", attrs={"target": "users"} ) for mention_tag in mentions_tags: - mentions.append(mention_tag['id']) + mentions.append(mention_tag["id"]) return list(set(mentions)) except Exception as e: return [] - + + def get_new_comment_mentions(new_value, old_value): - mentions_newer = extract_comment_mentions(new_value) if old_value is None: return mentions_newer - + mentions_older = extract_comment_mentions(old_value) # Getting Set Difference from mentions_newer new_mentions = [ - mention for mention in mentions_newer if mention not in mentions_older] + mention for mention in mentions_newer if mention not in mentions_older + ] return new_mentions -def createMentionNotification(project, notification_comment, issue, actor_id, mention_id, issue_id, activity): +def createMentionNotification( + project, + notification_comment, + issue, + actor_id, + mention_id, + issue_id, + activity, +): return Notification( workspace=project.workspace, sender="in_app:issue_activities:mentioned", @@ -178,16 +199,26 @@ def createMentionNotification(project, notification_comment, issue, actor_id, me "actor": str(activity.get("actor_id")), "new_value": str(activity.get("new_value")), "old_value": str(activity.get("old_value")), - } + }, }, ) @shared_task -def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance): +def notifications( + type, + issue_id, + project_id, + actor_id, + subscriber, + issue_activities_created, + requested_data, + current_instance, +): issue_activities_created = ( - json.loads( - issue_activities_created) if issue_activities_created is not None else None + json.loads(issue_activities_created) + if issue_activities_created is not None + else None ) if type not in [ "issue.activity.deleted", @@ -216,76 +247,110 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi # Get new mentions from the newer instance new_mentions = get_new_mentions( - requested_instance=requested_data, current_instance=current_instance) + requested_instance=requested_data, + current_instance=current_instance, + ) removed_mention = get_removed_mentions( - requested_instance=requested_data, current_instance=current_instance) - + requested_instance=requested_data, + current_instance=current_instance, + ) + comment_mentions = [] all_comment_mentions = [] # Get New Subscribers from the mentions of the newer instance - requested_mentions = extract_mentions( - issue_instance=requested_data) + requested_mentions = extract_mentions(issue_instance=requested_data) mention_subscribers = extract_mentions_as_subscribers( - project_id=project_id, issue_id=issue_id, mentions=requested_mentions) - + project_id=project_id, + issue_id=issue_id, + mentions=requested_mentions, + ) + for issue_activity in issue_activities_created: issue_comment = issue_activity.get("issue_comment") issue_comment_new_value = issue_activity.get("new_value") issue_comment_old_value = issue_activity.get("old_value") if issue_comment is not None: # TODO: Maybe save the comment mentions, so that in future, we can filter out the issues based on comment mentions as well. - - all_comment_mentions = all_comment_mentions + extract_comment_mentions(issue_comment_new_value) - - new_comment_mentions = get_new_comment_mentions(old_value=issue_comment_old_value, new_value=issue_comment_new_value) + + all_comment_mentions = ( + all_comment_mentions + + extract_comment_mentions(issue_comment_new_value) + ) + + new_comment_mentions = get_new_comment_mentions( + old_value=issue_comment_old_value, + new_value=issue_comment_new_value, + ) comment_mentions = comment_mentions + new_comment_mentions - - comment_mention_subscribers = extract_mentions_as_subscribers( project_id=project_id, issue_id=issue_id, mentions=all_comment_mentions) + + comment_mention_subscribers = extract_mentions_as_subscribers( + project_id=project_id, + issue_id=issue_id, + mentions=all_comment_mentions, + ) """ We will not send subscription activity notification to the below mentioned user sets - Those who have been newly mentioned in the issue description, we will send mention notification to them. - When the activity is a comment_created and there exist a mention in the comment, then we have to send the "mention_in_comment" notification - When the activity is a comment_updated and there exist a mention change, then also we have to send the "mention_in_comment" notification """ - + issue_assignees = list( IssueAssignee.objects.filter( - project_id=project_id, issue_id=issue_id) + project_id=project_id, issue_id=issue_id + ) .exclude(assignee_id__in=list(new_mentions + comment_mentions)) .values_list("assignee", flat=True) ) - + issue_subscribers = list( IssueSubscriber.objects.filter( - project_id=project_id, issue_id=issue_id) - .exclude(subscriber_id__in=list(new_mentions + comment_mentions + [actor_id])) + project_id=project_id, issue_id=issue_id + ) + .exclude( + subscriber_id__in=list( + new_mentions + comment_mentions + [actor_id] + ) + ) .values_list("subscriber", flat=True) ) issue = Issue.objects.filter(pk=issue_id).first() - if (issue.created_by_id is not None and str(issue.created_by_id) != str(actor_id)): + if issue.created_by_id is not None and str(issue.created_by_id) != str( + actor_id + ): issue_subscribers = issue_subscribers + [issue.created_by_id] if subscriber: # add the user to issue subscriber try: - if str(issue.created_by_id) != str(actor_id) and uuid.UUID(actor_id) not in issue_assignees: + if ( + str(issue.created_by_id) != str(actor_id) + and uuid.UUID(actor_id) not in issue_assignees + ): _ = IssueSubscriber.objects.get_or_create( - project_id=project_id, issue_id=issue_id, subscriber_id=actor_id + project_id=project_id, + issue_id=issue_id, + subscriber_id=actor_id, ) except Exception as e: pass project = Project.objects.get(pk=project_id) - issue_subscribers = list(set(issue_subscribers + issue_assignees) - {uuid.UUID(actor_id)}) + issue_subscribers = list( + set(issue_subscribers + issue_assignees) - {uuid.UUID(actor_id)} + ) for subscriber in issue_subscribers: if subscriber in issue_subscribers: sender = "in_app:issue_activities:subscribed" - if issue.created_by_id is not None and subscriber == issue.created_by_id: + if ( + issue.created_by_id is not None + and subscriber == issue.created_by_id + ): sender = "in_app:issue_activities:created" if subscriber in issue_assignees: sender = "in_app:issue_activities:assigned" @@ -293,12 +358,16 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi for issue_activity in issue_activities_created: # Do not send notification for description update if issue_activity.get("field") == "description": - continue; + continue issue_comment = issue_activity.get("issue_comment") if issue_comment is not None: issue_comment = IssueComment.objects.get( - id=issue_comment, issue_id=issue_id, project_id=project_id, workspace_id=project.workspace_id) - + id=issue_comment, + issue_id=issue_id, + project_id=project_id, + workspace_id=project.workspace_id, + ) + bulk_notifications.append( Notification( workspace=project.workspace, @@ -323,11 +392,16 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi "verb": str(issue_activity.get("verb")), "field": str(issue_activity.get("field")), "actor": str(issue_activity.get("actor_id")), - "new_value": str(issue_activity.get("new_value")), - "old_value": str(issue_activity.get("old_value")), + "new_value": str( + issue_activity.get("new_value") + ), + "old_value": str( + issue_activity.get("old_value") + ), "issue_comment": str( issue_comment.comment_stripped - if issue_activity.get("issue_comment") is not None + if issue_activity.get("issue_comment") + is not None else "" ), }, @@ -337,7 +411,8 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi # Add Mentioned as Issue Subscribers IssueSubscriber.objects.bulk_create( - mention_subscribers + comment_mention_subscribers, batch_size=100) + mention_subscribers + comment_mention_subscribers, batch_size=100 + ) last_activity = ( IssueActivity.objects.filter(issue_id=issue_id) @@ -346,9 +421,9 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi ) actor = User.objects.get(pk=actor_id) - + for mention_id in comment_mentions: - if (mention_id != actor_id): + if mention_id != actor_id: for issue_activity in issue_activities_created: notification = createMentionNotification( project=project, @@ -357,21 +432,20 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi actor_id=actor_id, mention_id=mention_id, issue_id=issue_id, - activity=issue_activity + activity=issue_activity, ) bulk_notifications.append(notification) - for mention_id in new_mentions: - if (mention_id != actor_id): + if mention_id != actor_id: if ( last_activity is not None and last_activity.field == "description" and actor_id == str(last_activity.actor_id) ): bulk_notifications.append( - Notification( - workspace=project.workspace, + Notification( + workspace=project.workspace, sender="in_app:issue_activities:mentioned", triggered_by_id=actor_id, receiver_id=mention_id, @@ -383,22 +457,24 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi "issue": { "id": str(issue_id), "name": str(issue.name), - "identifier": str(issue.project.identifier), + "identifier": str( + issue.project.identifier + ), "sequence_id": issue.sequence_id, "state_name": issue.state.name, - "state_group": issue.state.group, - }, - "issue_activity": { - "id": str(last_activity.id), - "verb": str(last_activity.verb), - "field": str(last_activity.field), - "actor": str(last_activity.actor_id), - "new_value": str(last_activity.new_value), - "old_value": str(last_activity.old_value), - }, - }, - ) - ) + "state_group": issue.state.group, + }, + "issue_activity": { + "id": str(last_activity.id), + "verb": str(last_activity.verb), + "field": str(last_activity.field), + "actor": str(last_activity.actor_id), + "new_value": str(last_activity.new_value), + "old_value": str(last_activity.old_value), + }, + }, + ) + ) else: for issue_activity in issue_activities_created: notification = createMentionNotification( @@ -408,15 +484,17 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi actor_id=actor_id, mention_id=mention_id, issue_id=issue_id, - activity=issue_activity + activity=issue_activity, ) bulk_notifications.append(notification) # save new mentions for the particular issue and remove the mentions that has been deleted from the description - update_mentions_for_issue(issue=issue, project=project, new_mentions=new_mentions, - removed_mention=removed_mention) - + update_mentions_for_issue( + issue=issue, + project=project, + new_mentions=new_mentions, + removed_mention=removed_mention, + ) + # Bulk create notifications Notification.objects.bulk_create(bulk_notifications, batch_size=100) - - diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index b9221855b..a986de332 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -15,6 +15,7 @@ from sentry_sdk import capture_exception from plane.db.models import Project, User, ProjectMemberInvite from plane.license.utils.instance_value import get_email_configuration + @shared_task def project_invitation(email, project_id, token, current_site, invitor): try: diff --git a/apiserver/plane/bgtasks/user_welcome_task.py b/apiserver/plane/bgtasks/user_welcome_task.py deleted file mode 100644 index 33f4b5686..000000000 --- a/apiserver/plane/bgtasks/user_welcome_task.py +++ /dev/null @@ -1,36 +0,0 @@ -# Django imports -from django.conf import settings - -# Third party imports -from celery import shared_task -from sentry_sdk import capture_exception -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError - -# Module imports -from plane.db.models import User - - -@shared_task -def send_welcome_slack(user_id, created, message): - try: - instance = User.objects.get(pk=user_id) - - if created and not instance.is_bot: - # Send message on slack as well - if settings.SLACK_BOT_TOKEN: - client = WebClient(token=settings.SLACK_BOT_TOKEN) - try: - _ = client.chat_postMessage( - channel="#trackers", - text=message, - ) - except SlackApiError as e: - print(f"Got an error: {e.response['error']}") - return - except Exception as e: - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) - capture_exception(e) - return diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index 3681f002d..34bba0cf8 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -189,7 +189,8 @@ def send_webhook(event, payload, kw, action, slug, bulk): pk__in=[ str(event.get("issue")) for event in payload ] - ).prefetch_related("issue_cycle", "issue_module"), many=True + ).prefetch_related("issue_cycle", "issue_module"), + many=True, ).data event = "issue" action = "PATCH" @@ -197,7 +198,9 @@ def send_webhook(event, payload, kw, action, slug, bulk): event_data = [ get_model_data( event=event, - event_id=payload.get("id") if isinstance(payload, dict) else None, + event_id=payload.get("id") + if isinstance(payload, dict) + else None, many=False, ) ] diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index 7039cb875..06dd6e8cd 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -36,7 +36,6 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): # The complete url including the domain abs_url = str(current_site) + relative_link - ( EMAIL_HOST, EMAIL_HOST_USER, @@ -83,17 +82,6 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): msg.attach_alternative(html_content, "text/html") msg.send() - # Send message on slack as well - if settings.SLACK_BOT_TOKEN: - client = WebClient(token=settings.SLACK_BOT_TOKEN) - try: - _ = client.chat_postMessage( - channel="#trackers", - text=f"{workspace_member_invite.email} has been invited to {workspace.name} as a {workspace_member_invite.role}", - ) - except SlackApiError as e: - print(f"Got an error: {e.response['error']}") - return except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist) as e: print("Workspace or WorkspaceMember Invite Does not exists") diff --git a/apiserver/plane/db/management/commands/create_bucket.py b/apiserver/plane/db/management/commands/create_bucket.py index 054523bf9..bdd0b7014 100644 --- a/apiserver/plane/db/management/commands/create_bucket.py +++ b/apiserver/plane/db/management/commands/create_bucket.py @@ -5,7 +5,8 @@ from botocore.exceptions import ClientError # Django imports from django.core.management import BaseCommand -from django.conf import settings +from django.conf import settings + class Command(BaseCommand): help = "Create the default bucket for the instance" @@ -13,23 +14,31 @@ class Command(BaseCommand): def set_bucket_public_policy(self, s3_client, bucket_name): public_policy = { "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": "*", - "Action": ["s3:GetObject"], - "Resource": [f"arn:aws:s3:::{bucket_name}/*"] - }] + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetObject"], + "Resource": [f"arn:aws:s3:::{bucket_name}/*"], + } + ], } try: s3_client.put_bucket_policy( - Bucket=bucket_name, - Policy=json.dumps(public_policy) + Bucket=bucket_name, Policy=json.dumps(public_policy) + ) + self.stdout.write( + self.style.SUCCESS( + f"Public read access policy set for bucket '{bucket_name}'." + ) ) - self.stdout.write(self.style.SUCCESS(f"Public read access policy set for bucket '{bucket_name}'.")) except ClientError as e: - self.stdout.write(self.style.ERROR(f"Error setting public read access policy: {e}")) - + self.stdout.write( + self.style.ERROR( + f"Error setting public read access policy: {e}" + ) + ) def handle(self, *args, **options): # Create a session using the credentials from Django settings @@ -39,7 +48,9 @@ class Command(BaseCommand): aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, ) # Create an S3 client using the session - s3_client = session.client('s3', endpoint_url=settings.AWS_S3_ENDPOINT_URL) + s3_client = session.client( + "s3", endpoint_url=settings.AWS_S3_ENDPOINT_URL + ) bucket_name = settings.AWS_STORAGE_BUCKET_NAME self.stdout.write(self.style.NOTICE("Checking bucket...")) @@ -49,23 +60,41 @@ class Command(BaseCommand): self.set_bucket_public_policy(s3_client, bucket_name) except ClientError as e: - error_code = int(e.response['Error']['Code']) + error_code = int(e.response["Error"]["Code"]) bucket_name = settings.AWS_STORAGE_BUCKET_NAME if error_code == 404: # Bucket does not exist, create it - self.stdout.write(self.style.WARNING(f"Bucket '{bucket_name}' does not exist. Creating bucket...")) + self.stdout.write( + self.style.WARNING( + f"Bucket '{bucket_name}' does not exist. Creating bucket..." + ) + ) try: s3_client.create_bucket(Bucket=bucket_name) - self.stdout.write(self.style.SUCCESS(f"Bucket '{bucket_name}' created successfully.")) + self.stdout.write( + self.style.SUCCESS( + f"Bucket '{bucket_name}' created successfully." + ) + ) self.set_bucket_public_policy(s3_client, bucket_name) except ClientError as create_error: - self.stdout.write(self.style.ERROR(f"Failed to create bucket: {create_error}")) + self.stdout.write( + self.style.ERROR( + f"Failed to create bucket: {create_error}" + ) + ) elif error_code == 403: # Access to the bucket is forbidden - self.stdout.write(self.style.ERROR(f"Access to the bucket '{bucket_name}' is forbidden. Check permissions.")) + self.stdout.write( + self.style.ERROR( + f"Access to the bucket '{bucket_name}' is forbidden. Check permissions." + ) + ) else: # Another ClientError occurred - self.stdout.write(self.style.ERROR(f"Failed to check bucket: {e}")) + self.stdout.write( + self.style.ERROR(f"Failed to check bucket: {e}") + ) except Exception as ex: # Handle any other exception - self.stdout.write(self.style.ERROR(f"An error occurred: {ex}")) \ No newline at end of file + self.stdout.write(self.style.ERROR(f"An error occurred: {ex}")) diff --git a/apiserver/plane/db/management/commands/reset_password.py b/apiserver/plane/db/management/commands/reset_password.py index a5b4c9cc8..d48c24b1c 100644 --- a/apiserver/plane/db/management/commands/reset_password.py +++ b/apiserver/plane/db/management/commands/reset_password.py @@ -35,7 +35,7 @@ class Command(BaseCommand): # get password for the user password = getpass.getpass("Password: ") confirm_password = getpass.getpass("Password (again): ") - + # If the passwords doesn't match raise error if password != confirm_password: self.stderr.write("Error: Your passwords didn't match.") @@ -50,5 +50,7 @@ class Command(BaseCommand): user.set_password(password) user.is_password_autoset = False user.save() - - self.stdout.write(self.style.SUCCESS(f"User password updated succesfully")) + + self.stdout.write( + self.style.SUCCESS(f"User password updated succesfully") + ) diff --git a/apiserver/plane/db/management/commands/wait_for_db.py b/apiserver/plane/db/management/commands/wait_for_db.py index 365452a7a..ec971f83a 100644 --- a/apiserver/plane/db/management/commands/wait_for_db.py +++ b/apiserver/plane/db/management/commands/wait_for_db.py @@ -2,18 +2,19 @@ import time from django.db import connections from django.db.utils import OperationalError from django.core.management import BaseCommand - + + class Command(BaseCommand): """Django command to pause execution until db is available""" - + def handle(self, *args, **options): - self.stdout.write('Waiting for database...') + self.stdout.write("Waiting for database...") db_conn = None while not db_conn: try: - db_conn = connections['default'] + db_conn = connections["default"] except OperationalError: - self.stdout.write('Database unavailable, waititng 1 second...') + self.stdout.write("Database unavailable, waititng 1 second...") time.sleep(1) - - self.stdout.write(self.style.SUCCESS('Database available!')) + + self.stdout.write(self.style.SUCCESS("Database available!")) diff --git a/apiserver/plane/db/migrations/0001_initial.py b/apiserver/plane/db/migrations/0001_initial.py index dd158f0a8..936d33fa5 100644 --- a/apiserver/plane/db/migrations/0001_initial.py +++ b/apiserver/plane/db/migrations/0001_initial.py @@ -10,695 +10,2481 @@ import uuid class Migration(migrations.Migration): - initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('username', models.CharField(max_length=128, unique=True)), - ('mobile_number', models.CharField(blank=True, max_length=255, null=True)), - ('email', models.CharField(blank=True, max_length=255, null=True, unique=True)), - ('first_name', models.CharField(blank=True, max_length=255)), - ('last_name', models.CharField(blank=True, max_length=255)), - ('avatar', models.CharField(blank=True, max_length=255)), - ('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('last_location', models.CharField(blank=True, max_length=255)), - ('created_location', models.CharField(blank=True, max_length=255)), - ('is_superuser', models.BooleanField(default=False)), - ('is_managed', models.BooleanField(default=False)), - ('is_password_expired', models.BooleanField(default=False)), - ('is_active', models.BooleanField(default=True)), - ('is_staff', models.BooleanField(default=False)), - ('is_email_verified', models.BooleanField(default=False)), - ('is_password_autoset', models.BooleanField(default=False)), - ('is_onboarded', models.BooleanField(default=False)), - ('token', models.CharField(blank=True, max_length=64)), - ('billing_address_country', models.CharField(default='INDIA', max_length=255)), - ('billing_address', models.JSONField(null=True)), - ('has_billing_address', models.BooleanField(default=False)), - ('user_timezone', models.CharField(default='Asia/Kolkata', max_length=255)), - ('last_active', models.DateTimeField(default=django.utils.timezone.now, null=True)), - ('last_login_time', models.DateTimeField(null=True)), - ('last_logout_time', models.DateTimeField(null=True)), - ('last_login_ip', models.CharField(blank=True, max_length=255)), - ('last_logout_ip', models.CharField(blank=True, max_length=255)), - ('last_login_medium', models.CharField(default='email', max_length=20)), - ('last_login_uagent', models.TextField(blank=True)), - ('token_updated_at', models.DateTimeField(null=True)), - ('last_workspace_id', models.UUIDField(null=True)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ( + "password", + models.CharField(max_length=128, verbose_name="password"), + ), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("username", models.CharField(max_length=128, unique=True)), + ( + "mobile_number", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "email", + models.CharField( + blank=True, max_length=255, null=True, unique=True + ), + ), + ("first_name", models.CharField(blank=True, max_length=255)), + ("last_name", models.CharField(blank=True, max_length=255)), + ("avatar", models.CharField(blank=True, max_length=255)), + ( + "date_joined", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "last_location", + models.CharField(blank=True, max_length=255), + ), + ( + "created_location", + models.CharField(blank=True, max_length=255), + ), + ("is_superuser", models.BooleanField(default=False)), + ("is_managed", models.BooleanField(default=False)), + ("is_password_expired", models.BooleanField(default=False)), + ("is_active", models.BooleanField(default=True)), + ("is_staff", models.BooleanField(default=False)), + ("is_email_verified", models.BooleanField(default=False)), + ("is_password_autoset", models.BooleanField(default=False)), + ("is_onboarded", models.BooleanField(default=False)), + ("token", models.CharField(blank=True, max_length=64)), + ( + "billing_address_country", + models.CharField(default="INDIA", max_length=255), + ), + ("billing_address", models.JSONField(null=True)), + ("has_billing_address", models.BooleanField(default=False)), + ( + "user_timezone", + models.CharField(default="Asia/Kolkata", max_length=255), + ), + ( + "last_active", + models.DateTimeField( + default=django.utils.timezone.now, null=True + ), + ), + ("last_login_time", models.DateTimeField(null=True)), + ("last_logout_time", models.DateTimeField(null=True)), + ( + "last_login_ip", + models.CharField(blank=True, max_length=255), + ), + ( + "last_logout_ip", + models.CharField(blank=True, max_length=255), + ), + ( + "last_login_medium", + models.CharField(default="email", max_length=20), + ), + ("last_login_uagent", models.TextField(blank=True)), + ("token_updated_at", models.DateTimeField(null=True)), + ("last_workspace_id", models.UUIDField(null=True)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'User', - 'verbose_name_plural': 'Users', - 'db_table': 'user', - 'ordering': ('-created_at',), + "verbose_name": "User", + "verbose_name_plural": "Users", + "db_table": "user", + "ordering": ("-created_at",), }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='Cycle', + name="Cycle", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='Cycle Name')), - ('description', models.TextField(blank=True, verbose_name='Cycle Description')), - ('start_date', models.DateField(verbose_name='Start Date')), - ('end_date', models.DateField(verbose_name='End Date')), - ('status', models.CharField(choices=[('started', 'Started'), ('completed', 'Completed')], max_length=255, verbose_name='Cycle Status')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cycle_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_by_cycle', to=settings.AUTH_USER_MODEL)), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField( + max_length=255, verbose_name="Cycle Name" + ), + ), + ( + "description", + models.TextField( + blank=True, verbose_name="Cycle Description" + ), + ), + ("start_date", models.DateField(verbose_name="Start Date")), + ("end_date", models.DateField(verbose_name="End Date")), + ( + "status", + models.CharField( + choices=[ + ("started", "Started"), + ("completed", "Completed"), + ], + max_length=255, + verbose_name="Cycle Status", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="cycle_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "owned_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="owned_by_cycle", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Cycle', - 'verbose_name_plural': 'Cycles', - 'db_table': 'cycle', - 'ordering': ('-created_at',), + "verbose_name": "Cycle", + "verbose_name_plural": "Cycles", + "db_table": "cycle", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='Issue', + name="Issue", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='Issue Name')), - ('description', models.JSONField(blank=True, verbose_name='Issue Description')), - ('priority', models.CharField(blank=True, choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], max_length=30, null=True, verbose_name='Issue Priority')), - ('start_date', models.DateField(blank=True, null=True)), - ('target_date', models.DateField(blank=True, null=True)), - ('sequence_id', models.IntegerField(default=1, verbose_name='Issue Sequence ID')), - ('attachments', django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), blank=True, default=list, size=10)), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField( + max_length=255, verbose_name="Issue Name" + ), + ), + ( + "description", + models.JSONField( + blank=True, verbose_name="Issue Description" + ), + ), + ( + "priority", + models.CharField( + blank=True, + choices=[ + ("urgent", "Urgent"), + ("high", "High"), + ("medium", "Medium"), + ("low", "Low"), + ], + max_length=30, + null=True, + verbose_name="Issue Priority", + ), + ), + ("start_date", models.DateField(blank=True, null=True)), + ("target_date", models.DateField(blank=True, null=True)), + ( + "sequence_id", + models.IntegerField( + default=1, verbose_name="Issue Sequence ID" + ), + ), + ( + "attachments", + django.contrib.postgres.fields.ArrayField( + base_field=models.URLField(), + blank=True, + default=list, + size=10, + ), + ), ], options={ - 'verbose_name': 'Issue', - 'verbose_name_plural': 'Issues', - 'db_table': 'issue', - 'ordering': ('-created_at',), + "verbose_name": "Issue", + "verbose_name_plural": "Issues", + "db_table": "issue", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='Project', + name="Project", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='Project Name')), - ('description', models.TextField(blank=True, verbose_name='Project Description')), - ('description_rt', models.JSONField(blank=True, null=True, verbose_name='Project Description RT')), - ('description_html', models.JSONField(blank=True, null=True, verbose_name='Project Description HTML')), - ('network', models.PositiveSmallIntegerField(choices=[(0, 'Secret'), (2, 'Public')], default=2)), - ('identifier', models.CharField(blank=True, max_length=5, null=True, verbose_name='Project Identifier')), - ('slug', models.SlugField(blank=True, max_length=100)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('default_assignee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='default_assignee', to=settings.AUTH_USER_MODEL)), - ('project_lead', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_lead', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField( + max_length=255, verbose_name="Project Name" + ), + ), + ( + "description", + models.TextField( + blank=True, verbose_name="Project Description" + ), + ), + ( + "description_rt", + models.JSONField( + blank=True, + null=True, + verbose_name="Project Description RT", + ), + ), + ( + "description_html", + models.JSONField( + blank=True, + null=True, + verbose_name="Project Description HTML", + ), + ), + ( + "network", + models.PositiveSmallIntegerField( + choices=[(0, "Secret"), (2, "Public")], default=2 + ), + ), + ( + "identifier", + models.CharField( + blank=True, + max_length=5, + null=True, + verbose_name="Project Identifier", + ), + ), + ("slug", models.SlugField(blank=True, max_length=100)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="project_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "default_assignee", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="default_assignee", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "project_lead", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_lead", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="project_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), ], options={ - 'verbose_name': 'Project', - 'verbose_name_plural': 'Projects', - 'db_table': 'project', - 'ordering': ('-created_at',), + "verbose_name": "Project", + "verbose_name_plural": "Projects", + "db_table": "project", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='Team', + name="Team", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='Team Name')), - ('description', models.TextField(blank=True, verbose_name='Team Description')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField(max_length=255, verbose_name="Team Name"), + ), + ( + "description", + models.TextField( + blank=True, verbose_name="Team Description" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="team_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), ], options={ - 'verbose_name': 'Team', - 'verbose_name_plural': 'Teams', - 'db_table': 'team', - 'ordering': ('-created_at',), + "verbose_name": "Team", + "verbose_name_plural": "Teams", + "db_table": "team", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='Workspace', + name="Workspace", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='Workspace Name')), - ('logo', models.URLField(blank=True, null=True, verbose_name='Logo')), - ('slug', models.SlugField(max_length=100, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspace_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner_workspace', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspace_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField( + max_length=255, verbose_name="Workspace Name" + ), + ), + ( + "logo", + models.URLField( + blank=True, null=True, verbose_name="Logo" + ), + ), + ("slug", models.SlugField(max_length=100, unique=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspace_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="owner_workspace", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspace_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), ], options={ - 'verbose_name': 'Workspace', - 'verbose_name_plural': 'Workspaces', - 'db_table': 'workspace', - 'ordering': ('-created_at',), - 'unique_together': {('name', 'owner')}, + "verbose_name": "Workspace", + "verbose_name_plural": "Workspaces", + "db_table": "workspace", + "ordering": ("-created_at",), + "unique_together": {("name", "owner")}, }, ), migrations.CreateModel( - name='WorkspaceMemberInvite', + name="WorkspaceMemberInvite", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('email', models.CharField(max_length=255)), - ('accepted', models.BooleanField(default=False)), - ('token', models.CharField(max_length=255)), - ('message', models.TextField(null=True)), - ('responded_at', models.DateTimeField(null=True)), - ('role', models.PositiveSmallIntegerField(choices=[(20, 'Owner'), (15, 'Admin'), (10, 'Member'), (5, 'Guest')], default=10)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacememberinvite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacememberinvite_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_member_invite', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("email", models.CharField(max_length=255)), + ("accepted", models.BooleanField(default=False)), + ("token", models.CharField(max_length=255)), + ("message", models.TextField(null=True)), + ("responded_at", models.DateTimeField(null=True)), + ( + "role", + models.PositiveSmallIntegerField( + choices=[ + (20, "Owner"), + (15, "Admin"), + (10, "Member"), + (5, "Guest"), + ], + default=10, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspacememberinvite_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspacememberinvite_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_member_invite", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Workspace Member Invite', - 'verbose_name_plural': 'Workspace Member Invites', - 'db_table': 'workspace_member_invite', - 'ordering': ('-created_at',), + "verbose_name": "Workspace Member Invite", + "verbose_name_plural": "Workspace Member Invites", + "db_table": "workspace_member_invite", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='View', + name="View", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='View Name')), - ('description', models.TextField(blank=True, verbose_name='View Description')), - ('query', models.JSONField(verbose_name='View Query')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='view_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_view', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='view_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_view', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField(max_length=255, verbose_name="View Name"), + ), + ( + "description", + models.TextField( + blank=True, verbose_name="View Description" + ), + ), + ("query", models.JSONField(verbose_name="View Query")), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="view_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_view", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="view_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_view", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'View', - 'verbose_name_plural': 'Views', - 'db_table': 'view', - 'ordering': ('-created_at',), + "verbose_name": "View", + "verbose_name_plural": "Views", + "db_table": "view", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='TimelineIssue', + name="TimelineIssue", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('sequence_id', models.FloatField(default=1.0)), - ('links', models.JSONField(blank=True, default=dict)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timelineissue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_timeline', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_timelineissue', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timelineissue_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_timelineissue', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("sequence_id", models.FloatField(default=1.0)), + ("links", models.JSONField(blank=True, default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="timelineissue_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_timeline", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_timelineissue", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="timelineissue_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_timelineissue", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Timeline Issue', - 'verbose_name_plural': 'Timeline Issues', - 'db_table': 'issue_timeline', - 'ordering': ('-created_at',), + "verbose_name": "Timeline Issue", + "verbose_name_plural": "Timeline Issues", + "db_table": "issue_timeline", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='TeamMember', + name="TeamMember", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teammember_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_member', to=settings.AUTH_USER_MODEL)), - ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_member', to='db.team')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teammember_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_member', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="teammember_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "member", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="team_member", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "team", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="team_member", + to="db.team", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="teammember_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="team_member", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Team Member', - 'verbose_name_plural': 'Team Members', - 'db_table': 'team_member', - 'ordering': ('-created_at',), - 'unique_together': {('team', 'member')}, + "verbose_name": "Team Member", + "verbose_name_plural": "Team Members", + "db_table": "team_member", + "ordering": ("-created_at",), + "unique_together": {("team", "member")}, }, ), migrations.AddField( - model_name='team', - name='members', - field=models.ManyToManyField(blank=True, related_name='members', through='db.TeamMember', to=settings.AUTH_USER_MODEL), + model_name="team", + name="members", + field=models.ManyToManyField( + blank=True, + related_name="members", + through="db.TeamMember", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='team', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='team_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + model_name="team", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="team_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), ), migrations.AddField( - model_name='team', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_team', to='db.workspace'), + model_name="team", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_team", + to="db.workspace", + ), ), migrations.CreateModel( - name='State', + name="State", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='State Name')), - ('description', models.TextField(blank=True, verbose_name='State Description')), - ('color', models.CharField(max_length=255, verbose_name='State Color')), - ('slug', models.SlugField(blank=True, max_length=100)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_state', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_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_state', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField( + max_length=255, verbose_name="State Name" + ), + ), + ( + "description", + models.TextField( + blank=True, verbose_name="State Description" + ), + ), + ( + "color", + models.CharField( + max_length=255, verbose_name="State Color" + ), + ), + ("slug", models.SlugField(blank=True, max_length=100)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="state_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_state", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="state_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_state", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'State', - 'verbose_name_plural': 'States', - 'db_table': 'state', - 'ordering': ('-created_at',), - 'unique_together': {('name', 'project')}, + "verbose_name": "State", + "verbose_name_plural": "States", + "db_table": "state", + "ordering": ("-created_at",), + "unique_together": {("name", "project")}, }, ), migrations.CreateModel( - name='SocialLoginConnection', + name="SocialLoginConnection", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('medium', models.CharField(choices=[('Google', 'google'), ('Github', 'github')], default=None, max_length=20)), - ('last_login_at', models.DateTimeField(default=django.utils.timezone.now, null=True)), - ('last_received_at', models.DateTimeField(default=django.utils.timezone.now, null=True)), - ('token_data', models.JSONField(null=True)), - ('extra_data', models.JSONField(null=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socialloginconnection_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socialloginconnection_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_login_connections', to=settings.AUTH_USER_MODEL)), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "medium", + models.CharField( + choices=[("Google", "google"), ("Github", "github")], + default=None, + max_length=20, + ), + ), + ( + "last_login_at", + models.DateTimeField( + default=django.utils.timezone.now, null=True + ), + ), + ( + "last_received_at", + models.DateTimeField( + default=django.utils.timezone.now, null=True + ), + ), + ("token_data", models.JSONField(null=True)), + ("extra_data", models.JSONField(null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="socialloginconnection_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="socialloginconnection_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_login_connections", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Social Login Connection', - 'verbose_name_plural': 'Social Login Connections', - 'db_table': 'social_login_connection', - 'ordering': ('-created_at',), + "verbose_name": "Social Login Connection", + "verbose_name_plural": "Social Login Connections", + "db_table": "social_login_connection", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='Shortcut', + name="Shortcut", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='Cycle Name')), - ('description', models.TextField(blank=True, verbose_name='Cycle Description')), - ('type', models.CharField(choices=[('repo', 'Repo'), ('direct', 'Direct')], max_length=255, verbose_name='Shortcut Type')), - ('url', models.URLField(blank=True, null=True, verbose_name='URL')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shortcut_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_shortcut', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shortcut_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_shortcut', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField( + max_length=255, verbose_name="Cycle Name" + ), + ), + ( + "description", + models.TextField( + blank=True, verbose_name="Cycle Description" + ), + ), + ( + "type", + models.CharField( + choices=[("repo", "Repo"), ("direct", "Direct")], + max_length=255, + verbose_name="Shortcut Type", + ), + ), + ( + "url", + models.URLField(blank=True, null=True, verbose_name="URL"), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="shortcut_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_shortcut", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="shortcut_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_shortcut", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Shortcut', - 'verbose_name_plural': 'Shortcuts', - 'db_table': 'shortcut', - 'ordering': ('-created_at',), + "verbose_name": "Shortcut", + "verbose_name_plural": "Shortcuts", + "db_table": "shortcut", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='ProjectMemberInvite', + name="ProjectMemberInvite", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('email', models.CharField(max_length=255)), - ('accepted', models.BooleanField(default=False)), - ('token', models.CharField(max_length=255)), - ('message', models.TextField(null=True)), - ('responded_at', models.DateTimeField(null=True)), - ('role', models.PositiveSmallIntegerField(choices=[(20, 'Admin'), (15, 'Member'), (10, 'Viewer'), (5, 'Guest')], default=10)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectmemberinvite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_projectmemberinvite', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectmemberinvite_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_projectmemberinvite', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("email", models.CharField(max_length=255)), + ("accepted", models.BooleanField(default=False)), + ("token", models.CharField(max_length=255)), + ("message", models.TextField(null=True)), + ("responded_at", models.DateTimeField(null=True)), + ( + "role", + models.PositiveSmallIntegerField( + choices=[ + (20, "Admin"), + (15, "Member"), + (10, "Viewer"), + (5, "Guest"), + ], + default=10, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="projectmemberinvite_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_projectmemberinvite", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="projectmemberinvite_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_projectmemberinvite", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Project Member Invite', - 'verbose_name_plural': 'Project Member Invites', - 'db_table': 'project_member_invite', - 'ordering': ('-created_at',), + "verbose_name": "Project Member Invite", + "verbose_name_plural": "Project Member Invites", + "db_table": "project_member_invite", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='ProjectIdentifier', + name="ProjectIdentifier", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('name', models.CharField(max_length=10)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectidentifier_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='project_identifier', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectidentifier_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ("name", models.CharField(max_length=10)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="projectidentifier_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_identifier", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="projectidentifier_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), ], options={ - 'verbose_name': 'Project Identifier', - 'verbose_name_plural': 'Project Identifiers', - 'db_table': 'project_identifier', - 'ordering': ('-created_at',), + "verbose_name": "Project Identifier", + "verbose_name_plural": "Project Identifiers", + "db_table": "project_identifier", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='project', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_project', to='db.workspace'), + model_name="project", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_project", + to="db.workspace", + ), ), migrations.CreateModel( - name='Label', + name="Label", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255)), - ('description', models.TextField(blank=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='label_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_label', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='label_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_label', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="label_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_label", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="label_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_label", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Label', - 'verbose_name_plural': 'Labels', - 'db_table': 'label', - 'ordering': ('-created_at',), + "verbose_name": "Label", + "verbose_name_plural": "Labels", + "db_table": "label", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='IssueSequence', + name="IssueSequence", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('sequence', models.PositiveBigIntegerField(default=1)), - ('deleted', models.BooleanField(default=False)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuesequence_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_sequence', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuesequence', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuesequence_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_issuesequence', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("sequence", models.PositiveBigIntegerField(default=1)), + ("deleted", models.BooleanField(default=False)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issuesequence_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_sequence", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issuesequence", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issuesequence_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_issuesequence", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue Sequence', - 'verbose_name_plural': 'Issue Sequences', - 'db_table': 'issue_sequence', - 'ordering': ('-created_at',), + "verbose_name": "Issue Sequence", + "verbose_name_plural": "Issue Sequences", + "db_table": "issue_sequence", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='IssueProperty', + name="IssueProperty", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('properties', models.JSONField(default=dict)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueproperty_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueproperty', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueproperty_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='issue_property_user', to=settings.AUTH_USER_MODEL)), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueproperty', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("properties", models.JSONField(default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueproperty_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issueproperty", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueproperty_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_property_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_issueproperty", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue Property', - 'verbose_name_plural': 'Issue Properties', - 'db_table': 'issue_property', - 'ordering': ('-created_at',), + "verbose_name": "Issue Property", + "verbose_name_plural": "Issue Properties", + "db_table": "issue_property", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='IssueLabel', + name="IssueLabel", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuelabel_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='label_issue', to='db.issue')), - ('label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='label_issue', to='db.label')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuelabel', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuelabel_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_issuelabel', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issuelabel_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="label_issue", + to="db.issue", + ), + ), + ( + "label", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="label_issue", + to="db.label", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issuelabel", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issuelabel_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_issuelabel", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue Label', - 'verbose_name_plural': 'Issue Labels', - 'db_table': 'issue_label', - 'ordering': ('-created_at',), + "verbose_name": "Issue Label", + "verbose_name_plural": "Issue Labels", + "db_table": "issue_label", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='IssueComment', + name="IssueComment", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('comment', models.TextField(blank=True, verbose_name='Comment')), - ('attachments', django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), blank=True, default=list, size=10)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuecomment_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuecomment', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuecomment_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_issuecomment', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "comment", + models.TextField(blank=True, verbose_name="Comment"), + ), + ( + "attachments", + django.contrib.postgres.fields.ArrayField( + base_field=models.URLField(), + blank=True, + default=list, + size=10, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issuecomment_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issuecomment", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issuecomment_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_issuecomment", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue Comment', - 'verbose_name_plural': 'Issue Comments', - 'db_table': 'issue_comment', - 'ordering': ('-created_at',), + "verbose_name": "Issue Comment", + "verbose_name_plural": "Issue Comments", + "db_table": "issue_comment", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='IssueBlocker', + name="IssueBlocker", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('block', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocker_issues', to='db.issue')), - ('blocked_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocked_issues', to='db.issue')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueblocker_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueblocker', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueblocker_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_issueblocker', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "block", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="blocker_issues", + to="db.issue", + ), + ), + ( + "blocked_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="blocked_issues", + to="db.issue", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueblocker_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issueblocker", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueblocker_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_issueblocker", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue Blocker', - 'verbose_name_plural': 'Issue Blockers', - 'db_table': 'issue_blocker', - 'ordering': ('-created_at',), + "verbose_name": "Issue Blocker", + "verbose_name_plural": "Issue Blockers", + "db_table": "issue_blocker", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='IssueAssignee', + name="IssueAssignee", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('assignee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_assignee', to=settings.AUTH_USER_MODEL)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueassignee_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_assignee', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueassignee', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueassignee_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_issueassignee', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "assignee", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_assignee", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueassignee_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_assignee", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issueassignee", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueassignee_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_issueassignee", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue Assignee', - 'verbose_name_plural': 'Issue Assignees', - 'db_table': 'issue_assignee', - 'ordering': ('-created_at',), - 'unique_together': {('issue', 'assignee')}, + "verbose_name": "Issue Assignee", + "verbose_name_plural": "Issue Assignees", + "db_table": "issue_assignee", + "ordering": ("-created_at",), + "unique_together": {("issue", "assignee")}, }, ), migrations.CreateModel( - name='IssueActivity', + name="IssueActivity", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('verb', models.CharField(default='created', max_length=255, verbose_name='Action')), - ('field', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field Name')), - ('old_value', models.CharField(blank=True, max_length=255, null=True, verbose_name='Old Value')), - ('new_value', models.CharField(blank=True, max_length=255, null=True, verbose_name='New Value')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), - ('attachments', django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), blank=True, default=list, size=10)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueactivity_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_activity', to='db.issue')), - ('issue_comment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_comment', to='db.issuecomment')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueactivity', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueactivity_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_issueactivity', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "verb", + models.CharField( + default="created", + max_length=255, + verbose_name="Action", + ), + ), + ( + "field", + models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="Field Name", + ), + ), + ( + "old_value", + models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="Old Value", + ), + ), + ( + "new_value", + models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="New Value", + ), + ), + ( + "comment", + models.TextField(blank=True, verbose_name="Comment"), + ), + ( + "attachments", + django.contrib.postgres.fields.ArrayField( + base_field=models.URLField(), + blank=True, + default=list, + size=10, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueactivity_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_activity", + to="db.issue", + ), + ), + ( + "issue_comment", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_comment", + to="db.issuecomment", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issueactivity", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueactivity_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_issueactivity", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue Activity', - 'verbose_name_plural': 'Issue Activities', - 'db_table': 'issue_activity', - 'ordering': ('-created_at',), + "verbose_name": "Issue Activity", + "verbose_name_plural": "Issue Activities", + "db_table": "issue_activity", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='issue', - name='assignees', - field=models.ManyToManyField(blank=True, related_name='assignee', through='db.IssueAssignee', to=settings.AUTH_USER_MODEL), + model_name="issue", + name="assignees", + field=models.ManyToManyField( + blank=True, + related_name="assignee", + through="db.IssueAssignee", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='issue', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + model_name="issue", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), ), migrations.AddField( - model_name='issue', - name='labels', - field=models.ManyToManyField(blank=True, related_name='labels', through='db.IssueLabel', to='db.Label'), + model_name="issue", + name="labels", + field=models.ManyToManyField( + blank=True, + related_name="labels", + through="db.IssueLabel", + to="db.Label", + ), ), migrations.AddField( - model_name='issue', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_issue', to='db.issue'), + model_name="issue", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="parent_issue", + to="db.issue", + ), ), migrations.AddField( - model_name='issue', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issue', to='db.project'), + model_name="issue", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issue", + to="db.project", + ), ), migrations.AddField( - model_name='issue', - name='state', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='state_issue', to='db.state'), + model_name="issue", + name="state", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="state_issue", + to="db.state", + ), ), migrations.AddField( - model_name='issue', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + model_name="issue", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), ), migrations.AddField( - model_name='issue', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issue', to='db.workspace'), + model_name="issue", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_issue", + to="db.workspace", + ), ), migrations.CreateModel( - name='FileAsset', + name="FileAsset", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('attributes', models.JSONField(default=dict)), - ('asset', models.FileField(upload_to='library-assets')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fileasset_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fileasset_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("attributes", models.JSONField(default=dict)), + ("asset", models.FileField(upload_to="library-assets")), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fileasset_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fileasset_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), ], options={ - 'verbose_name': 'File Asset', - 'verbose_name_plural': 'File Assets', - 'db_table': 'file_asset', - 'ordering': ('-created_at',), + "verbose_name": "File Asset", + "verbose_name_plural": "File Assets", + "db_table": "file_asset", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='CycleIssue', + name="CycleIssue", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cycleissue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_cycle', to='db.cycle')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_cycle', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_cycleissue', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cycleissue_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_cycleissue', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="cycleissue_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "cycle", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_cycle", + to="db.cycle", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_cycle", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_cycleissue", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="cycleissue_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_cycleissue", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Cycle Issue', - 'verbose_name_plural': 'Cycle Issues', - 'db_table': 'cycle_issue', - 'ordering': ('-created_at',), + "verbose_name": "Cycle Issue", + "verbose_name_plural": "Cycle Issues", + "db_table": "cycle_issue", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='cycle', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_cycle', to='db.project'), + model_name="cycle", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_cycle", + to="db.project", + ), ), migrations.AddField( - model_name='cycle', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cycle_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + model_name="cycle", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="cycle_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), ), migrations.AddField( - model_name='cycle', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_cycle', to='db.workspace'), + model_name="cycle", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_cycle", + to="db.workspace", + ), ), migrations.CreateModel( - name='WorkspaceMember', + name="WorkspaceMember", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('role', models.PositiveSmallIntegerField(choices=[(20, 'Owner'), (15, 'Admin'), (10, 'Member'), (5, 'Guest')], default=10)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacemember_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='member_workspace', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacemember_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_member', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "role", + models.PositiveSmallIntegerField( + choices=[ + (20, "Owner"), + (15, "Admin"), + (10, "Member"), + (5, "Guest"), + ], + default=10, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspacemember_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "member", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="member_workspace", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspacemember_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_member", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Workspace Member', - 'verbose_name_plural': 'Workspace Members', - 'db_table': 'workspace_member', - 'ordering': ('-created_at',), - 'unique_together': {('workspace', 'member')}, + "verbose_name": "Workspace Member", + "verbose_name_plural": "Workspace Members", + "db_table": "workspace_member", + "ordering": ("-created_at",), + "unique_together": {("workspace", "member")}, }, ), migrations.AlterUniqueTogether( - name='team', - unique_together={('name', 'workspace')}, + name="team", + unique_together={("name", "workspace")}, ), migrations.CreateModel( - name='ProjectMember', + name="ProjectMember", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('comment', models.TextField(blank=True, null=True)), - ('role', models.PositiveSmallIntegerField(choices=[(20, 'Admin'), (15, 'Member'), (10, 'Viewer'), (5, 'Guest')], default=10)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectmember_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('member', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='member_project', to=settings.AUTH_USER_MODEL)), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_projectmember', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectmember_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_projectmember', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("comment", models.TextField(blank=True, null=True)), + ( + "role", + models.PositiveSmallIntegerField( + choices=[ + (20, "Admin"), + (15, "Member"), + (10, "Viewer"), + (5, "Guest"), + ], + default=10, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="projectmember_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "member", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="member_project", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_projectmember", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="projectmember_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_projectmember", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Project Member', - 'verbose_name_plural': 'Project Members', - 'db_table': 'project_member', - 'ordering': ('-created_at',), - 'unique_together': {('project', 'member')}, + "verbose_name": "Project Member", + "verbose_name_plural": "Project Members", + "db_table": "project_member", + "ordering": ("-created_at",), + "unique_together": {("project", "member")}, }, ), migrations.AlterUniqueTogether( - name='project', - unique_together={('name', 'workspace')}, + name="project", + unique_together={("name", "workspace")}, ), ] diff --git a/apiserver/plane/db/migrations/0002_auto_20221104_2239.py b/apiserver/plane/db/migrations/0002_auto_20221104_2239.py index 9c25c4518..d69ef1a71 100644 --- a/apiserver/plane/db/migrations/0002_auto_20221104_2239.py +++ b/apiserver/plane/db/migrations/0002_auto_20221104_2239.py @@ -6,49 +6,66 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('db', '0001_initial'), + ("db", "0001_initial"), ] operations = [ migrations.AlterModelOptions( - name='state', - options={'ordering': ('sequence',), 'verbose_name': 'State', 'verbose_name_plural': 'States'}, + name="state", + options={ + "ordering": ("sequence",), + "verbose_name": "State", + "verbose_name_plural": "States", + }, ), migrations.RenameField( - model_name='project', - old_name='description_rt', - new_name='description_text', + model_name="project", + old_name="description_rt", + new_name="description_text", ), migrations.AddField( - model_name='issueactivity', - name='actor', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_activities', to=settings.AUTH_USER_MODEL), + model_name="issueactivity", + name="actor", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_activities", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='issuecomment', - name='actor', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL), + model_name="issuecomment", + name="actor", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='state', - name='sequence', + model_name="state", + name="sequence", field=models.PositiveIntegerField(default=65535), ), migrations.AddField( - model_name='workspace', - name='company_size', + model_name="workspace", + name="company_size", field=models.PositiveIntegerField(default=10), ), migrations.AddField( - model_name='workspacemember', - name='company_role', + model_name="workspacemember", + name="company_role", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='cycleissue', - name='issue', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='issue_cycle', to='db.issue'), + model_name="cycleissue", + name="issue", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_cycle", + to="db.issue", + ), ), ] diff --git a/apiserver/plane/db/migrations/0003_auto_20221109_2320.py b/apiserver/plane/db/migrations/0003_auto_20221109_2320.py index 3adac35a7..763d52eb6 100644 --- a/apiserver/plane/db/migrations/0003_auto_20221109_2320.py +++ b/apiserver/plane/db/migrations/0003_auto_20221109_2320.py @@ -6,19 +6,22 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('db', '0002_auto_20221104_2239'), + ("db", "0002_auto_20221104_2239"), ] operations = [ migrations.AlterField( - model_name='issueproperty', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_property_user', to=settings.AUTH_USER_MODEL), + model_name="issueproperty", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_property_user", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterUniqueTogether( - name='issueproperty', - unique_together={('user', 'project')}, + name="issueproperty", + unique_together={("user", "project")}, ), ] diff --git a/apiserver/plane/db/migrations/0004_alter_state_sequence.py b/apiserver/plane/db/migrations/0004_alter_state_sequence.py index 0d4616aea..f3489449c 100644 --- a/apiserver/plane/db/migrations/0004_alter_state_sequence.py +++ b/apiserver/plane/db/migrations/0004_alter_state_sequence.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0003_auto_20221109_2320'), + ("db", "0003_auto_20221109_2320"), ] operations = [ migrations.AlterField( - model_name='state', - name='sequence', + model_name="state", + name="sequence", field=models.FloatField(default=65535), ), ] diff --git a/apiserver/plane/db/migrations/0005_auto_20221114_2127.py b/apiserver/plane/db/migrations/0005_auto_20221114_2127.py index 14c280e26..8ab63a22a 100644 --- a/apiserver/plane/db/migrations/0005_auto_20221114_2127.py +++ b/apiserver/plane/db/migrations/0005_auto_20221114_2127.py @@ -4,20 +4,23 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0004_alter_state_sequence'), + ("db", "0004_alter_state_sequence"), ] operations = [ migrations.AlterField( - model_name='cycle', - name='end_date', - field=models.DateField(blank=True, null=True, verbose_name='End Date'), + model_name="cycle", + name="end_date", + field=models.DateField( + blank=True, null=True, verbose_name="End Date" + ), ), migrations.AlterField( - model_name='cycle', - name='start_date', - field=models.DateField(blank=True, null=True, verbose_name='Start Date'), + model_name="cycle", + name="start_date", + field=models.DateField( + blank=True, null=True, verbose_name="Start Date" + ), ), ] diff --git a/apiserver/plane/db/migrations/0006_alter_cycle_status.py b/apiserver/plane/db/migrations/0006_alter_cycle_status.py index f49e263fb..3121f4fe5 100644 --- a/apiserver/plane/db/migrations/0006_alter_cycle_status.py +++ b/apiserver/plane/db/migrations/0006_alter_cycle_status.py @@ -4,15 +4,23 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0005_auto_20221114_2127'), + ("db", "0005_auto_20221114_2127"), ] operations = [ migrations.AlterField( - model_name='cycle', - name='status', - field=models.CharField(choices=[('draft', 'Draft'), ('started', 'Started'), ('completed', 'Completed')], default='draft', max_length=255, verbose_name='Cycle Status'), + model_name="cycle", + name="status", + field=models.CharField( + choices=[ + ("draft", "Draft"), + ("started", "Started"), + ("completed", "Completed"), + ], + default="draft", + max_length=255, + verbose_name="Cycle Status", + ), ), ] diff --git a/apiserver/plane/db/migrations/0007_label_parent.py b/apiserver/plane/db/migrations/0007_label_parent.py index 03e660473..6e67a3c94 100644 --- a/apiserver/plane/db/migrations/0007_label_parent.py +++ b/apiserver/plane/db/migrations/0007_label_parent.py @@ -5,15 +5,20 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('db', '0006_alter_cycle_status'), + ("db", "0006_alter_cycle_status"), ] operations = [ migrations.AddField( - model_name='label', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_label', to='db.label'), + model_name="label", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="parent_label", + to="db.label", + ), ), ] diff --git a/apiserver/plane/db/migrations/0008_label_colour.py b/apiserver/plane/db/migrations/0008_label_colour.py index 9e630969d..3ca6b91c1 100644 --- a/apiserver/plane/db/migrations/0008_label_colour.py +++ b/apiserver/plane/db/migrations/0008_label_colour.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0007_label_parent'), + ("db", "0007_label_parent"), ] operations = [ migrations.AddField( - model_name='label', - name='colour', + model_name="label", + name="colour", field=models.CharField(blank=True, max_length=255), ), ] diff --git a/apiserver/plane/db/migrations/0009_auto_20221208_0310.py b/apiserver/plane/db/migrations/0009_auto_20221208_0310.py index 077ab7e82..829baaa62 100644 --- a/apiserver/plane/db/migrations/0009_auto_20221208_0310.py +++ b/apiserver/plane/db/migrations/0009_auto_20221208_0310.py @@ -4,20 +4,29 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0008_label_colour'), + ("db", "0008_label_colour"), ] operations = [ migrations.AddField( - model_name='projectmember', - name='view_props', + model_name="projectmember", + name="view_props", field=models.JSONField(null=True), ), migrations.AddField( - model_name='state', - name='group', - field=models.CharField(choices=[('backlog', 'Backlog'), ('unstarted', 'Unstarted'), ('started', 'Started'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='backlog', max_length=20), + model_name="state", + name="group", + field=models.CharField( + choices=[ + ("backlog", "Backlog"), + ("unstarted", "Unstarted"), + ("started", "Started"), + ("completed", "Completed"), + ("cancelled", "Cancelled"), + ], + default="backlog", + max_length=20, + ), ), ] diff --git a/apiserver/plane/db/migrations/0010_auto_20221213_0037.py b/apiserver/plane/db/migrations/0010_auto_20221213_0037.py index e8579b5ff..1672a10ab 100644 --- a/apiserver/plane/db/migrations/0010_auto_20221213_0037.py +++ b/apiserver/plane/db/migrations/0010_auto_20221213_0037.py @@ -5,28 +5,37 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('db', '0009_auto_20221208_0310'), + ("db", "0009_auto_20221208_0310"), ] operations = [ migrations.AddField( - model_name='projectidentifier', - name='workspace', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_identifiers', to='db.workspace'), + model_name="projectidentifier", + name="workspace", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_identifiers", + to="db.workspace", + ), ), migrations.AlterField( - model_name='project', - name='identifier', - field=models.CharField(max_length=5, verbose_name='Project Identifier'), + model_name="project", + name="identifier", + field=models.CharField( + max_length=5, verbose_name="Project Identifier" + ), ), migrations.AlterUniqueTogether( - name='project', - unique_together={('name', 'workspace'), ('identifier', 'workspace')}, + name="project", + unique_together={ + ("name", "workspace"), + ("identifier", "workspace"), + }, ), migrations.AlterUniqueTogether( - name='projectidentifier', - unique_together={('name', 'workspace')}, + name="projectidentifier", + unique_together={("name", "workspace")}, ), ] diff --git a/apiserver/plane/db/migrations/0011_auto_20221222_2357.py b/apiserver/plane/db/migrations/0011_auto_20221222_2357.py index deeb1cc2f..b52df3012 100644 --- a/apiserver/plane/db/migrations/0011_auto_20221222_2357.py +++ b/apiserver/plane/db/migrations/0011_auto_20221222_2357.py @@ -8,122 +8,341 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0010_auto_20221213_0037'), + ("db", "0010_auto_20221213_0037"), ] operations = [ migrations.CreateModel( - name='Module', + name="Module", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='Module Name')), - ('description', models.TextField(blank=True, verbose_name='Module Description')), - ('description_text', models.JSONField(blank=True, null=True, verbose_name='Module Description RT')), - ('description_html', models.JSONField(blank=True, null=True, verbose_name='Module Description HTML')), - ('start_date', models.DateField(null=True)), - ('target_date', models.DateField(null=True)), - ('status', models.CharField(choices=[('backlog', 'Backlog'), ('planned', 'Planned'), ('in-progress', 'In Progress'), ('paused', 'Paused'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='planned', max_length=20)), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField( + max_length=255, verbose_name="Module Name" + ), + ), + ( + "description", + models.TextField( + blank=True, verbose_name="Module Description" + ), + ), + ( + "description_text", + models.JSONField( + blank=True, + null=True, + verbose_name="Module Description RT", + ), + ), + ( + "description_html", + models.JSONField( + blank=True, + null=True, + verbose_name="Module Description HTML", + ), + ), + ("start_date", models.DateField(null=True)), + ("target_date", models.DateField(null=True)), + ( + "status", + models.CharField( + choices=[ + ("backlog", "Backlog"), + ("planned", "Planned"), + ("in-progress", "In Progress"), + ("paused", "Paused"), + ("completed", "Completed"), + ("cancelled", "Cancelled"), + ], + default="planned", + max_length=20, + ), + ), ], options={ - 'verbose_name': 'Module', - 'verbose_name_plural': 'Modules', - 'db_table': 'module', - 'ordering': ('-created_at',), + "verbose_name": "Module", + "verbose_name_plural": "Modules", + "db_table": "module", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='project', - name='icon', + model_name="project", + name="icon", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='projectmember', - name='default_props', - field=models.JSONField(default=plane.db.models.project.get_default_props), + model_name="projectmember", + name="default_props", + field=models.JSONField( + default=plane.db.models.project.get_default_props + ), ), migrations.AddField( - model_name='user', - name='my_issues_prop', + model_name="user", + name="my_issues_prop", field=models.JSONField(null=True), ), migrations.CreateModel( - name='ModuleMember', + name="ModuleMember", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulemember_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='db.module')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_modulemember', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulemember_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_modulemember', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="modulemember_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "member", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "module", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="db.module", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_modulemember", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="modulemember_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_modulemember", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Module Member', - 'verbose_name_plural': 'Module Members', - 'db_table': 'module_member', - 'ordering': ('-created_at',), - 'unique_together': {('module', 'member')}, + "verbose_name": "Module Member", + "verbose_name_plural": "Module Members", + "db_table": "module_member", + "ordering": ("-created_at",), + "unique_together": {("module", "member")}, }, ), migrations.AddField( - model_name='module', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='module_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + model_name="module", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="module_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), ), migrations.AddField( - model_name='module', - name='lead', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='module_leads', to=settings.AUTH_USER_MODEL), + model_name="module", + name="lead", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="module_leads", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='module', - name='members', - field=models.ManyToManyField(blank=True, related_name='module_members', through='db.ModuleMember', to=settings.AUTH_USER_MODEL), + model_name="module", + name="members", + field=models.ManyToManyField( + blank=True, + related_name="module_members", + through="db.ModuleMember", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='module', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_module', to='db.project'), + model_name="module", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_module", + to="db.project", + ), ), migrations.AddField( - model_name='module', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='module_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + model_name="module", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="module_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), ), migrations.AddField( - model_name='module', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_module', to='db.workspace'), + model_name="module", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_module", + to="db.workspace", + ), ), migrations.CreateModel( - name='ModuleIssue', + name="ModuleIssue", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='moduleissue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_module', to='db.issue')), - ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_module', to='db.module')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_moduleissue', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='moduleissue_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_moduleissue', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="moduleissue_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_module", + to="db.issue", + ), + ), + ( + "module", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_module", + to="db.module", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_moduleissue", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="moduleissue_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_moduleissue", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Module Issue', - 'verbose_name_plural': 'Module Issues', - 'db_table': 'module_issues', - 'ordering': ('-created_at',), - 'unique_together': {('module', 'issue')}, + "verbose_name": "Module Issue", + "verbose_name_plural": "Module Issues", + "db_table": "module_issues", + "ordering": ("-created_at",), + "unique_together": {("module", "issue")}, }, ), migrations.AlterUniqueTogether( - name='module', - unique_together={('name', 'project')}, + name="module", + unique_together={("name", "project")}, ), ] diff --git a/apiserver/plane/db/migrations/0012_auto_20230104_0117.py b/apiserver/plane/db/migrations/0012_auto_20230104_0117.py index b1ff63fe1..bc767dd5d 100644 --- a/apiserver/plane/db/migrations/0012_auto_20230104_0117.py +++ b/apiserver/plane/db/migrations/0012_auto_20230104_0117.py @@ -7,166 +7,228 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0011_auto_20221222_2357'), + ("db", "0011_auto_20221222_2357"), ] operations = [ migrations.AddField( - model_name='issueactivity', - name='new_identifier', + model_name="issueactivity", + name="new_identifier", field=models.UUIDField(null=True), ), migrations.AddField( - model_name='issueactivity', - name='old_identifier', + model_name="issueactivity", + name="old_identifier", field=models.UUIDField(null=True), ), migrations.AlterField( - model_name='moduleissue', - name='issue', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='issue_module', to='db.issue'), + model_name="moduleissue", + name="issue", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_module", + to="db.issue", + ), ), migrations.AlterUniqueTogether( - name='moduleissue', + name="moduleissue", unique_together=set(), ), migrations.AlterModelTable( - name='cycle', - table='cycles', + name="cycle", + table="cycles", ), migrations.AlterModelTable( - name='cycleissue', - table='cycle_issues', + name="cycleissue", + table="cycle_issues", ), migrations.AlterModelTable( - name='fileasset', - table='file_assets', + name="fileasset", + table="file_assets", ), migrations.AlterModelTable( - name='issue', - table='issues', + name="issue", + table="issues", ), migrations.AlterModelTable( - name='issueactivity', - table='issue_activities', + name="issueactivity", + table="issue_activities", ), migrations.AlterModelTable( - name='issueassignee', - table='issue_assignees', + name="issueassignee", + table="issue_assignees", ), migrations.AlterModelTable( - name='issueblocker', - table='issue_blockers', + name="issueblocker", + table="issue_blockers", ), migrations.AlterModelTable( - name='issuecomment', - table='issue_comments', + name="issuecomment", + table="issue_comments", ), migrations.AlterModelTable( - name='issuelabel', - table='issue_labels', + name="issuelabel", + table="issue_labels", ), migrations.AlterModelTable( - name='issueproperty', - table='issue_properties', + name="issueproperty", + table="issue_properties", ), migrations.AlterModelTable( - name='issuesequence', - table='issue_sequences', + name="issuesequence", + table="issue_sequences", ), migrations.AlterModelTable( - name='label', - table='labels', + name="label", + table="labels", ), migrations.AlterModelTable( - name='module', - table='modules', + name="module", + table="modules", ), migrations.AlterModelTable( - name='modulemember', - table='module_members', + name="modulemember", + table="module_members", ), migrations.AlterModelTable( - name='project', - table='projects', + name="project", + table="projects", ), migrations.AlterModelTable( - name='projectidentifier', - table='project_identifiers', + name="projectidentifier", + table="project_identifiers", ), migrations.AlterModelTable( - name='projectmember', - table='project_members', + name="projectmember", + table="project_members", ), migrations.AlterModelTable( - name='projectmemberinvite', - table='project_member_invites', + name="projectmemberinvite", + table="project_member_invites", ), migrations.AlterModelTable( - name='shortcut', - table='shortcuts', + name="shortcut", + table="shortcuts", ), migrations.AlterModelTable( - name='socialloginconnection', - table='social_login_connections', + name="socialloginconnection", + table="social_login_connections", ), migrations.AlterModelTable( - name='state', - table='states', + name="state", + table="states", ), migrations.AlterModelTable( - name='team', - table='teams', + name="team", + table="teams", ), migrations.AlterModelTable( - name='teammember', - table='team_members', + name="teammember", + table="team_members", ), migrations.AlterModelTable( - name='timelineissue', - table='issue_timelines', + name="timelineissue", + table="issue_timelines", ), migrations.AlterModelTable( - name='user', - table='users', + name="user", + table="users", ), migrations.AlterModelTable( - name='view', - table='views', + name="view", + table="views", ), migrations.AlterModelTable( - name='workspace', - table='workspaces', + name="workspace", + table="workspaces", ), migrations.AlterModelTable( - name='workspacemember', - table='workspace_members', + name="workspacemember", + table="workspace_members", ), migrations.AlterModelTable( - name='workspacememberinvite', - table='workspace_member_invites', + name="workspacememberinvite", + table="workspace_member_invites", ), migrations.CreateModel( - name='ModuleLink', + name="ModuleLink", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('title', models.CharField(max_length=255, null=True)), - ('url', models.URLField()), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulelink_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_module', to='db.module')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_modulelink', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulelink_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_modulelink', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("title", models.CharField(max_length=255, null=True)), + ("url", models.URLField()), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="modulelink_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "module", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="link_module", + to="db.module", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_modulelink", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="modulelink_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_modulelink", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Module Link', - 'verbose_name_plural': 'Module Links', - 'db_table': 'module_links', - 'ordering': ('-created_at',), + "verbose_name": "Module Link", + "verbose_name_plural": "Module Links", + "db_table": "module_links", + "ordering": ("-created_at",), }, ), ] diff --git a/apiserver/plane/db/migrations/0013_auto_20230107_0041.py b/apiserver/plane/db/migrations/0013_auto_20230107_0041.py index c75537fc1..786e6cb5d 100644 --- a/apiserver/plane/db/migrations/0013_auto_20230107_0041.py +++ b/apiserver/plane/db/migrations/0013_auto_20230107_0041.py @@ -4,35 +4,34 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0012_auto_20230104_0117'), + ("db", "0012_auto_20230104_0117"), ] operations = [ migrations.AddField( - model_name='issue', - name='description_html', + model_name="issue", + name="description_html", field=models.TextField(blank=True), ), migrations.AddField( - model_name='issue', - name='description_stripped', + model_name="issue", + name="description_stripped", field=models.TextField(blank=True), ), migrations.AddField( - model_name='user', - name='role', + model_name="user", + name="role", field=models.CharField(blank=True, max_length=300, null=True), ), migrations.AddField( - model_name='workspacemember', - name='view_props', + model_name="workspacemember", + name="view_props", field=models.JSONField(blank=True, null=True), ), migrations.AlterField( - model_name='issue', - name='description', + model_name="issue", + name="description", field=models.JSONField(blank=True), ), ] diff --git a/apiserver/plane/db/migrations/0014_alter_workspacememberinvite_unique_together.py b/apiserver/plane/db/migrations/0014_alter_workspacememberinvite_unique_together.py index b1786c9c1..5642ae15d 100644 --- a/apiserver/plane/db/migrations/0014_alter_workspacememberinvite_unique_together.py +++ b/apiserver/plane/db/migrations/0014_alter_workspacememberinvite_unique_together.py @@ -4,14 +4,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('db', '0013_auto_20230107_0041'), + ("db", "0013_auto_20230107_0041"), ] operations = [ migrations.AlterUniqueTogether( - name='workspacememberinvite', - unique_together={('email', 'workspace')}, + name="workspacememberinvite", + unique_together={("email", "workspace")}, ), ] diff --git a/apiserver/plane/db/migrations/0015_auto_20230107_1636.py b/apiserver/plane/db/migrations/0015_auto_20230107_1636.py index e3f5dc26a..903c78b05 100644 --- a/apiserver/plane/db/migrations/0015_auto_20230107_1636.py +++ b/apiserver/plane/db/migrations/0015_auto_20230107_1636.py @@ -4,25 +4,24 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0014_alter_workspacememberinvite_unique_together'), + ("db", "0014_alter_workspacememberinvite_unique_together"), ] operations = [ migrations.RenameField( - model_name='issuecomment', - old_name='comment', - new_name='comment_stripped', + model_name="issuecomment", + old_name="comment", + new_name="comment_stripped", ), migrations.AddField( - model_name='issuecomment', - name='comment_html', + model_name="issuecomment", + name="comment_html", field=models.TextField(blank=True), ), migrations.AddField( - model_name='issuecomment', - name='comment_json', + model_name="issuecomment", + name="comment_json", field=models.JSONField(blank=True, null=True), ), ] diff --git a/apiserver/plane/db/migrations/0016_auto_20230107_1735.py b/apiserver/plane/db/migrations/0016_auto_20230107_1735.py index 073c1e117..a22dc9a62 100644 --- a/apiserver/plane/db/migrations/0016_auto_20230107_1735.py +++ b/apiserver/plane/db/migrations/0016_auto_20230107_1735.py @@ -6,20 +6,27 @@ import plane.db.models.asset class Migration(migrations.Migration): - dependencies = [ - ('db', '0015_auto_20230107_1636'), + ("db", "0015_auto_20230107_1636"), ] operations = [ migrations.AddField( - model_name='fileasset', - name='workspace', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='db.workspace'), + model_name="fileasset", + name="workspace", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="assets", + to="db.workspace", + ), ), migrations.AlterField( - model_name='fileasset', - name='asset', - field=models.FileField(upload_to=plane.db.models.asset.get_upload_path, validators=[plane.db.models.asset.file_size]), + model_name="fileasset", + name="asset", + field=models.FileField( + upload_to=plane.db.models.asset.get_upload_path, + validators=[plane.db.models.asset.file_size], + ), ), ] diff --git a/apiserver/plane/db/migrations/0017_alter_workspace_unique_together.py b/apiserver/plane/db/migrations/0017_alter_workspace_unique_together.py index c6bfc2145..1ab721a3e 100644 --- a/apiserver/plane/db/migrations/0017_alter_workspace_unique_together.py +++ b/apiserver/plane/db/migrations/0017_alter_workspace_unique_together.py @@ -4,14 +4,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('db', '0016_auto_20230107_1735'), + ("db", "0016_auto_20230107_1735"), ] operations = [ migrations.AlterUniqueTogether( - name='workspace', + name="workspace", unique_together=set(), ), ] diff --git a/apiserver/plane/db/migrations/0018_auto_20230130_0119.py b/apiserver/plane/db/migrations/0018_auto_20230130_0119.py index 03eaeacd7..32f886539 100644 --- a/apiserver/plane/db/migrations/0018_auto_20230130_0119.py +++ b/apiserver/plane/db/migrations/0018_auto_20230130_0119.py @@ -8,50 +8,112 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0017_alter_workspace_unique_together'), + ("db", "0017_alter_workspace_unique_together"), ] operations = [ migrations.AddField( - model_name='user', - name='is_bot', + model_name="user", + name="is_bot", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='issue', - name='description', + model_name="issue", + name="description", field=models.JSONField(blank=True, null=True), ), migrations.AlterField( - model_name='issue', - name='description_html', + model_name="issue", + name="description_html", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='issue', - name='description_stripped', + model_name="issue", + name="description_stripped", field=models.TextField(blank=True, null=True), ), migrations.CreateModel( - name='APIToken', + name="APIToken", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('token', models.CharField(default=plane.db.models.api.generate_token, max_length=255, unique=True)), - ('label', models.CharField(default=plane.db.models.api.generate_label_token, max_length=255)), - ('user_type', models.PositiveSmallIntegerField(choices=[(0, 'Human'), (1, 'Bot')], default=0)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='apitoken_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='apitoken_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bot_tokens', to=settings.AUTH_USER_MODEL)), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "token", + models.CharField( + default=plane.db.models.api.generate_token, + max_length=255, + unique=True, + ), + ), + ( + "label", + models.CharField( + default=plane.db.models.api.generate_label_token, + max_length=255, + ), + ), + ( + "user_type", + models.PositiveSmallIntegerField( + choices=[(0, "Human"), (1, "Bot")], default=0 + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="apitoken_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="apitoken_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="bot_tokens", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'API Token', - 'verbose_name_plural': 'API Tokems', - 'db_table': 'api_tokens', - 'ordering': ('-created_at',), + "verbose_name": "API Token", + "verbose_name_plural": "API Tokems", + "db_table": "api_tokens", + "ordering": ("-created_at",), }, ), ] diff --git a/apiserver/plane/db/migrations/0019_auto_20230131_0049.py b/apiserver/plane/db/migrations/0019_auto_20230131_0049.py index 38412aa9e..63545f497 100644 --- a/apiserver/plane/db/migrations/0019_auto_20230131_0049.py +++ b/apiserver/plane/db/migrations/0019_auto_20230131_0049.py @@ -4,20 +4,23 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0018_auto_20230130_0119'), + ("db", "0018_auto_20230130_0119"), ] operations = [ migrations.AlterField( - model_name='issueactivity', - name='new_value', - field=models.TextField(blank=True, null=True, verbose_name='New Value'), + model_name="issueactivity", + name="new_value", + field=models.TextField( + blank=True, null=True, verbose_name="New Value" + ), ), migrations.AlterField( - model_name='issueactivity', - name='old_value', - field=models.TextField(blank=True, null=True, verbose_name='Old Value'), + model_name="issueactivity", + name="old_value", + field=models.TextField( + blank=True, null=True, verbose_name="Old Value" + ), ), ] diff --git a/apiserver/plane/db/migrations/0020_auto_20230214_0118.py b/apiserver/plane/db/migrations/0020_auto_20230214_0118.py index 192764078..4269f53b3 100644 --- a/apiserver/plane/db/migrations/0020_auto_20230214_0118.py +++ b/apiserver/plane/db/migrations/0020_auto_20230214_0118.py @@ -5,65 +5,69 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('db', '0019_auto_20230131_0049'), + ("db", "0019_auto_20230131_0049"), ] operations = [ migrations.RenameField( - model_name='label', - old_name='colour', - new_name='color', + model_name="label", + old_name="colour", + new_name="color", ), migrations.AddField( - model_name='apitoken', - name='workspace', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to='db.workspace'), + model_name="apitoken", + name="workspace", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="api_tokens", + to="db.workspace", + ), ), migrations.AddField( - model_name='issue', - name='completed_at', + model_name="issue", + name="completed_at", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='issue', - name='sort_order', + model_name="issue", + name="sort_order", field=models.FloatField(default=65535), ), migrations.AddField( - model_name='project', - name='cycle_view', + model_name="project", + name="cycle_view", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='project', - name='module_view', + model_name="project", + name="module_view", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='state', - name='default', + model_name="state", + name="default", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='issue', - name='description', + model_name="issue", + name="description", field=models.JSONField(blank=True, default=dict), ), migrations.AlterField( - model_name='issue', - name='description_html', - field=models.TextField(blank=True, default='

'), + model_name="issue", + name="description_html", + field=models.TextField(blank=True, default="

"), ), migrations.AlterField( - model_name='issuecomment', - name='comment_html', - field=models.TextField(blank=True, default='

'), + model_name="issuecomment", + name="comment_html", + field=models.TextField(blank=True, default="

"), ), migrations.AlterField( - model_name='issuecomment', - name='comment_json', + model_name="issuecomment", + name="comment_json", field=models.JSONField(blank=True, default=dict), ), ] diff --git a/apiserver/plane/db/migrations/0021_auto_20230223_0104.py b/apiserver/plane/db/migrations/0021_auto_20230223_0104.py index bae6a086a..0dc052c28 100644 --- a/apiserver/plane/db/migrations/0021_auto_20230223_0104.py +++ b/apiserver/plane/db/migrations/0021_auto_20230223_0104.py @@ -7,179 +7,616 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0020_auto_20230214_0118'), + ("db", "0020_auto_20230214_0118"), ] operations = [ migrations.CreateModel( - name='GithubRepository', + name="GithubRepository", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=500)), - ('url', models.URLField(null=True)), - ('config', models.JSONField(default=dict)), - ('repository_id', models.BigIntegerField()), - ('owner', models.CharField(max_length=500)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='githubrepository_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_githubrepository', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='githubrepository_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_githubrepository', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=500)), + ("url", models.URLField(null=True)), + ("config", models.JSONField(default=dict)), + ("repository_id", models.BigIntegerField()), + ("owner", models.CharField(max_length=500)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="githubrepository_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_githubrepository", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="githubrepository_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_githubrepository", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Repository', - 'verbose_name_plural': 'Repositories', - 'db_table': 'github_repositories', - 'ordering': ('-created_at',), + "verbose_name": "Repository", + "verbose_name_plural": "Repositories", + "db_table": "github_repositories", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='Integration', + name="Integration", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('title', models.CharField(max_length=400)), - ('provider', models.CharField(max_length=400, unique=True)), - ('network', models.PositiveIntegerField(choices=[(1, 'Private'), (2, 'Public')], default=1)), - ('description', models.JSONField(default=dict)), - ('author', models.CharField(blank=True, max_length=400)), - ('webhook_url', models.TextField(blank=True)), - ('webhook_secret', models.TextField(blank=True)), - ('redirect_url', models.TextField(blank=True)), - ('metadata', models.JSONField(default=dict)), - ('verified', models.BooleanField(default=False)), - ('avatar_url', models.URLField(blank=True, null=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='integration_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='integration_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("title", models.CharField(max_length=400)), + ("provider", models.CharField(max_length=400, unique=True)), + ( + "network", + models.PositiveIntegerField( + choices=[(1, "Private"), (2, "Public")], default=1 + ), + ), + ("description", models.JSONField(default=dict)), + ("author", models.CharField(blank=True, max_length=400)), + ("webhook_url", models.TextField(blank=True)), + ("webhook_secret", models.TextField(blank=True)), + ("redirect_url", models.TextField(blank=True)), + ("metadata", models.JSONField(default=dict)), + ("verified", models.BooleanField(default=False)), + ("avatar_url", models.URLField(blank=True, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="integration_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="integration_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), ], options={ - 'verbose_name': 'Integration', - 'verbose_name_plural': 'Integrations', - 'db_table': 'integrations', - 'ordering': ('-created_at',), + "verbose_name": "Integration", + "verbose_name_plural": "Integrations", + "db_table": "integrations", + "ordering": ("-created_at",), }, ), migrations.AlterField( - model_name='issueactivity', - name='issue', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_activity', to='db.issue'), + model_name="issueactivity", + name="issue", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_activity", + to="db.issue", + ), ), migrations.CreateModel( - name='WorkspaceIntegration', + name="WorkspaceIntegration", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('metadata', models.JSONField(default=dict)), - ('config', models.JSONField(default=dict)), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integrations', to=settings.AUTH_USER_MODEL)), - ('api_token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integrations', to='db.apitoken')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspaceintegration_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integrated_workspaces', to='db.integration')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspaceintegration_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_integrations', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("metadata", models.JSONField(default=dict)), + ("config", models.JSONField(default=dict)), + ( + "actor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="integrations", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "api_token", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="integrations", + to="db.apitoken", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspaceintegration_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "integration", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="integrated_workspaces", + to="db.integration", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspaceintegration_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_integrations", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Workspace Integration', - 'verbose_name_plural': 'Workspace Integrations', - 'db_table': 'workspace_integrations', - 'ordering': ('-created_at',), - 'unique_together': {('workspace', 'integration')}, + "verbose_name": "Workspace Integration", + "verbose_name_plural": "Workspace Integrations", + "db_table": "workspace_integrations", + "ordering": ("-created_at",), + "unique_together": {("workspace", "integration")}, }, ), migrations.CreateModel( - name='IssueLink', + name="IssueLink", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('title', models.CharField(max_length=255, null=True)), - ('url', models.URLField()), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuelink_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_link', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuelink', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuelink_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_issuelink', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("title", models.CharField(max_length=255, null=True)), + ("url", models.URLField()), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issuelink_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_link", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issuelink", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issuelink_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_issuelink", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue Link', - 'verbose_name_plural': 'Issue Links', - 'db_table': 'issue_links', - 'ordering': ('-created_at',), + "verbose_name": "Issue Link", + "verbose_name_plural": "Issue Links", + "db_table": "issue_links", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='GithubRepositorySync', + name="GithubRepositorySync", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('credentials', models.JSONField(default=dict)), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_syncs', to=settings.AUTH_USER_MODEL)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='githubrepositorysync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('label', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='repo_syncs', to='db.label')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_githubrepositorysync', to='db.project')), - ('repository', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='syncs', to='db.githubrepository')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='githubrepositorysync_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_githubrepositorysync', to='db.workspace')), - ('workspace_integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='github_syncs', to='db.workspaceintegration')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("credentials", models.JSONField(default=dict)), + ( + "actor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_syncs", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="githubrepositorysync_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "label", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="repo_syncs", + to="db.label", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_githubrepositorysync", + to="db.project", + ), + ), + ( + "repository", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="syncs", + to="db.githubrepository", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="githubrepositorysync_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_githubrepositorysync", + to="db.workspace", + ), + ), + ( + "workspace_integration", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="github_syncs", + to="db.workspaceintegration", + ), + ), ], options={ - 'verbose_name': 'Github Repository Sync', - 'verbose_name_plural': 'Github Repository Syncs', - 'db_table': 'github_repository_syncs', - 'ordering': ('-created_at',), - 'unique_together': {('project', 'repository')}, + "verbose_name": "Github Repository Sync", + "verbose_name_plural": "Github Repository Syncs", + "db_table": "github_repository_syncs", + "ordering": ("-created_at",), + "unique_together": {("project", "repository")}, }, ), migrations.CreateModel( - name='GithubIssueSync', + name="GithubIssueSync", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('repo_issue_id', models.BigIntegerField()), - ('github_issue_id', models.BigIntegerField()), - ('issue_url', models.URLField()), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='githubissuesync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='github_syncs', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_githubissuesync', to='db.project')), - ('repository_sync', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_syncs', to='db.githubrepositorysync')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='githubissuesync_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_githubissuesync', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("repo_issue_id", models.BigIntegerField()), + ("github_issue_id", models.BigIntegerField()), + ("issue_url", models.URLField()), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="githubissuesync_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="github_syncs", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_githubissuesync", + to="db.project", + ), + ), + ( + "repository_sync", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_syncs", + to="db.githubrepositorysync", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="githubissuesync_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_githubissuesync", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Github Issue Sync', - 'verbose_name_plural': 'Github Issue Syncs', - 'db_table': 'github_issue_syncs', - 'ordering': ('-created_at',), - 'unique_together': {('repository_sync', 'issue')}, + "verbose_name": "Github Issue Sync", + "verbose_name_plural": "Github Issue Syncs", + "db_table": "github_issue_syncs", + "ordering": ("-created_at",), + "unique_together": {("repository_sync", "issue")}, }, ), migrations.CreateModel( - name='GithubCommentSync', + name="GithubCommentSync", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('repo_comment_id', models.BigIntegerField()), - ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comment_syncs', to='db.issuecomment')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='githubcommentsync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue_sync', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comment_syncs', to='db.githubissuesync')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_githubcommentsync', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='githubcommentsync_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_githubcommentsync', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("repo_comment_id", models.BigIntegerField()), + ( + "comment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comment_syncs", + to="db.issuecomment", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="githubcommentsync_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue_sync", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comment_syncs", + to="db.githubissuesync", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_githubcommentsync", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="githubcommentsync_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_githubcommentsync", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Github Comment Sync', - 'verbose_name_plural': 'Github Comment Syncs', - 'db_table': 'github_comment_syncs', - 'ordering': ('-created_at',), - 'unique_together': {('issue_sync', 'comment')}, + "verbose_name": "Github Comment Sync", + "verbose_name_plural": "Github Comment Syncs", + "db_table": "github_comment_syncs", + "ordering": ("-created_at",), + "unique_together": {("issue_sync", "comment")}, }, ), ] diff --git a/apiserver/plane/db/migrations/0022_auto_20230307_0304.py b/apiserver/plane/db/migrations/0022_auto_20230307_0304.py index 25a8eef61..69bd577d7 100644 --- a/apiserver/plane/db/migrations/0022_auto_20230307_0304.py +++ b/apiserver/plane/db/migrations/0022_auto_20230307_0304.py @@ -7,95 +7,285 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0021_auto_20230223_0104'), + ("db", "0021_auto_20230223_0104"), ] operations = [ migrations.RemoveField( - model_name='cycle', - name='status', + model_name="cycle", + name="status", ), migrations.RemoveField( - model_name='project', - name='slug', + model_name="project", + name="slug", ), migrations.AddField( - model_name='issuelink', - name='metadata', + model_name="issuelink", + name="metadata", field=models.JSONField(default=dict), ), migrations.AddField( - model_name='modulelink', - name='metadata', + model_name="modulelink", + name="metadata", field=models.JSONField(default=dict), ), migrations.AddField( - model_name='project', - name='cover_image', + model_name="project", + name="cover_image", field=models.URLField(blank=True, null=True), ), migrations.CreateModel( - name='ProjectFavorite', + name="ProjectFavorite", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectfavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_projectfavorite', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectfavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_favorites', to=settings.AUTH_USER_MODEL)), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_projectfavorite', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="projectfavorite_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_projectfavorite", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="projectfavorite_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_favorites", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_projectfavorite", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Project Favorite', - 'verbose_name_plural': 'Project Favorites', - 'db_table': 'project_favorites', - 'ordering': ('-created_at',), - 'unique_together': {('project', 'user')}, + "verbose_name": "Project Favorite", + "verbose_name_plural": "Project Favorites", + "db_table": "project_favorites", + "ordering": ("-created_at",), + "unique_together": {("project", "user")}, }, ), migrations.CreateModel( - name='ModuleFavorite', + name="ModuleFavorite", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulefavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_favorites', to='db.module')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_modulefavorite', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulefavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_favorites', to=settings.AUTH_USER_MODEL)), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_modulefavorite', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="modulefavorite_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "module", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="module_favorites", + to="db.module", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_modulefavorite", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="modulefavorite_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="module_favorites", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_modulefavorite", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Module Favorite', - 'verbose_name_plural': 'Module Favorites', - 'db_table': 'module_favorites', - 'ordering': ('-created_at',), - 'unique_together': {('module', 'user')}, + "verbose_name": "Module Favorite", + "verbose_name_plural": "Module Favorites", + "db_table": "module_favorites", + "ordering": ("-created_at",), + "unique_together": {("module", "user")}, }, ), migrations.CreateModel( - name='CycleFavorite', + name="CycleFavorite", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cyclefavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_favorites', to='db.cycle')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_cyclefavorite', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cyclefavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_favorites', to=settings.AUTH_USER_MODEL)), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_cyclefavorite', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="cyclefavorite_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "cycle", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="cycle_favorites", + to="db.cycle", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_cyclefavorite", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="cyclefavorite_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="cycle_favorites", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_cyclefavorite", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Cycle Favorite', - 'verbose_name_plural': 'Cycle Favorites', - 'db_table': 'cycle_favorites', - 'ordering': ('-created_at',), - 'unique_together': {('cycle', 'user')}, + "verbose_name": "Cycle Favorite", + "verbose_name_plural": "Cycle Favorites", + "db_table": "cycle_favorites", + "ordering": ("-created_at",), + "unique_together": {("cycle", "user")}, }, ), ] diff --git a/apiserver/plane/db/migrations/0023_auto_20230316_0040.py b/apiserver/plane/db/migrations/0023_auto_20230316_0040.py index c6985866c..6f6103cae 100644 --- a/apiserver/plane/db/migrations/0023_auto_20230316_0040.py +++ b/apiserver/plane/db/migrations/0023_auto_20230316_0040.py @@ -7,86 +7,299 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0022_auto_20230307_0304'), + ("db", "0022_auto_20230307_0304"), ] operations = [ migrations.CreateModel( - name='Importer', + name="Importer", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('service', models.CharField(choices=[('github', 'GitHub')], max_length=50)), - ('status', models.CharField(choices=[('queued', 'Queued'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='queued', max_length=50)), - ('metadata', models.JSONField(default=dict)), - ('config', models.JSONField(default=dict)), - ('data', models.JSONField(default=dict)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='importer_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('initiated_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='imports', to=settings.AUTH_USER_MODEL)), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_importer', to='db.project')), - ('token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='importer', to='db.apitoken')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='importer_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_importer', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "service", + models.CharField( + choices=[("github", "GitHub")], max_length=50 + ), + ), + ( + "status", + models.CharField( + choices=[ + ("queued", "Queued"), + ("processing", "Processing"), + ("completed", "Completed"), + ("failed", "Failed"), + ], + default="queued", + max_length=50, + ), + ), + ("metadata", models.JSONField(default=dict)), + ("config", models.JSONField(default=dict)), + ("data", models.JSONField(default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="importer_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "initiated_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="imports", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_importer", + to="db.project", + ), + ), + ( + "token", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="importer", + to="db.apitoken", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="importer_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_importer", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Importer', - 'verbose_name_plural': 'Importers', - 'db_table': 'importers', - 'ordering': ('-created_at',), + "verbose_name": "Importer", + "verbose_name_plural": "Importers", + "db_table": "importers", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='IssueView', + name="IssueView", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='View Name')), - ('description', models.TextField(blank=True, verbose_name='View Description')), - ('query', models.JSONField(verbose_name='View Query')), - ('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)), - ('query_data', models.JSONField(default=dict)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueview_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueview', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueview_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_issueview', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField(max_length=255, verbose_name="View Name"), + ), + ( + "description", + models.TextField( + blank=True, verbose_name="View Description" + ), + ), + ("query", models.JSONField(verbose_name="View Query")), + ( + "access", + models.PositiveSmallIntegerField( + choices=[(0, "Private"), (1, "Public")], default=1 + ), + ), + ("query_data", models.JSONField(default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueview_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issueview", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueview_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_issueview", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue View', - 'verbose_name_plural': 'Issue Views', - 'db_table': 'issue_views', - 'ordering': ('-created_at',), + "verbose_name": "Issue View", + "verbose_name_plural": "Issue Views", + "db_table": "issue_views", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='IssueViewFavorite', + name="IssueViewFavorite", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueviewfavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueviewfavorite', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueviewfavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_view_favorites', to=settings.AUTH_USER_MODEL)), - ('view', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='view_favorites', to='db.issueview')), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issueviewfavorite', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueviewfavorite_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issueviewfavorite", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueviewfavorite_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_view_favorites", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "view", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="view_favorites", + to="db.issueview", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_issueviewfavorite", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'View Favorite', - 'verbose_name_plural': 'View Favorites', - 'db_table': 'view_favorites', - 'ordering': ('-created_at',), - 'unique_together': {('view', 'user')}, + "verbose_name": "View Favorite", + "verbose_name_plural": "View Favorites", + "db_table": "view_favorites", + "ordering": ("-created_at",), + "unique_together": {("view", "user")}, }, ), migrations.AlterUniqueTogether( - name='label', - unique_together={('name', 'project')}, + name="label", + unique_together={("name", "project")}, ), migrations.DeleteModel( - name='View', + name="View", ), ] diff --git a/apiserver/plane/db/migrations/0024_auto_20230322_0138.py b/apiserver/plane/db/migrations/0024_auto_20230322_0138.py index 65880891a..7a95d519e 100644 --- a/apiserver/plane/db/migrations/0024_auto_20230322_0138.py +++ b/apiserver/plane/db/migrations/0024_auto_20230322_0138.py @@ -7,107 +7,308 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0023_auto_20230316_0040'), + ("db", "0023_auto_20230316_0040"), ] operations = [ migrations.CreateModel( - name='Page', + name="Page", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255)), - ('description', models.JSONField(blank=True, default=dict)), - ('description_html', models.TextField(blank=True, default='

')), - ('description_stripped', models.TextField(blank=True, null=True)), - ('access', models.PositiveSmallIntegerField(choices=[(0, 'Public'), (1, 'Private')], default=0)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='page_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pages', to=settings.AUTH_USER_MODEL)), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.JSONField(blank=True, default=dict)), + ( + "description_html", + models.TextField(blank=True, default="

"), + ), + ( + "description_stripped", + models.TextField(blank=True, null=True), + ), + ( + "access", + models.PositiveSmallIntegerField( + choices=[(0, "Public"), (1, "Private")], default=0 + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="page_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "owned_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="pages", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Page', - 'verbose_name_plural': 'Pages', - 'db_table': 'pages', - 'ordering': ('-created_at',), + "verbose_name": "Page", + "verbose_name_plural": "Pages", + "db_table": "pages", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='project', - name='issue_views_view', + model_name="project", + name="issue_views_view", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='importer', - name='service', - field=models.CharField(choices=[('github', 'GitHub'), ('jira', 'Jira')], max_length=50), + model_name="importer", + name="service", + field=models.CharField( + choices=[("github", "GitHub"), ("jira", "Jira")], max_length=50 + ), ), migrations.AlterField( - model_name='project', - name='cover_image', + model_name="project", + name="cover_image", field=models.URLField(blank=True, max_length=800, null=True), ), migrations.CreateModel( - name='PageBlock', + name="PageBlock", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255)), - ('description', models.JSONField(blank=True, default=dict)), - ('description_html', models.TextField(blank=True, default='

')), - ('description_stripped', models.TextField(blank=True, null=True)), - ('completed_at', models.DateTimeField(null=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pageblock_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='blocks', to='db.issue')), - ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocks', to='db.page')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_pageblock', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pageblock_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_pageblock', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.JSONField(blank=True, default=dict)), + ( + "description_html", + models.TextField(blank=True, default="

"), + ), + ( + "description_stripped", + models.TextField(blank=True, null=True), + ), + ("completed_at", models.DateTimeField(null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="pageblock_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="blocks", + to="db.issue", + ), + ), + ( + "page", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="blocks", + to="db.page", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_pageblock", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="pageblock_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_pageblock", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Page Block', - 'verbose_name_plural': 'Page Blocks', - 'db_table': 'page_blocks', - 'ordering': ('-created_at',), + "verbose_name": "Page Block", + "verbose_name_plural": "Page Blocks", + "db_table": "page_blocks", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='page', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_page', to='db.project'), + model_name="page", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_page", + to="db.project", + ), ), migrations.AddField( - model_name='page', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='page_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + model_name="page", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="page_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), ), migrations.AddField( - model_name='page', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_page', to='db.workspace'), + model_name="page", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_page", + to="db.workspace", + ), ), migrations.CreateModel( - name='PageFavorite', + name="PageFavorite", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pagefavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_favorites', to='db.page')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_pagefavorite', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pagefavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_favorites', to=settings.AUTH_USER_MODEL)), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_pagefavorite', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="pagefavorite_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "page", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="page_favorites", + to="db.page", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_pagefavorite", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="pagefavorite_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="page_favorites", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_pagefavorite", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Page Favorite', - 'verbose_name_plural': 'Page Favorites', - 'db_table': 'page_favorites', - 'ordering': ('-created_at',), - 'unique_together': {('page', 'user')}, + "verbose_name": "Page Favorite", + "verbose_name_plural": "Page Favorites", + "db_table": "page_favorites", + "ordering": ("-created_at",), + "unique_together": {("page", "user")}, }, ), ] diff --git a/apiserver/plane/db/migrations/0025_auto_20230331_0203.py b/apiserver/plane/db/migrations/0025_auto_20230331_0203.py index 1097a4612..702d74cfc 100644 --- a/apiserver/plane/db/migrations/0025_auto_20230331_0203.py +++ b/apiserver/plane/db/migrations/0025_auto_20230331_0203.py @@ -7,55 +7,125 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0024_auto_20230322_0138'), + ("db", "0024_auto_20230322_0138"), ] operations = [ migrations.AddField( - model_name='page', - name='color', + model_name="page", + name="color", field=models.CharField(blank=True, max_length=255), ), migrations.AddField( - model_name='pageblock', - name='sort_order', + model_name="pageblock", + name="sort_order", field=models.FloatField(default=65535), ), migrations.AddField( - model_name='pageblock', - name='sync', + model_name="pageblock", + name="sync", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='project', - name='page_view', + model_name="project", + name="page_view", field=models.BooleanField(default=True), ), migrations.CreateModel( - name='PageLabel', + name="PageLabel", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pagelabel_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_labels', to='db.label')), - ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_labels', to='db.page')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_pagelabel', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pagelabel_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_pagelabel', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="pagelabel_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "label", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="page_labels", + to="db.label", + ), + ), + ( + "page", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="page_labels", + to="db.page", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_pagelabel", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="pagelabel_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_pagelabel", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Page Label', - 'verbose_name_plural': 'Page Labels', - 'db_table': 'page_labels', - 'ordering': ('-created_at',), + "verbose_name": "Page Label", + "verbose_name_plural": "Page Labels", + "db_table": "page_labels", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='page', - name='labels', - field=models.ManyToManyField(blank=True, related_name='pages', through='db.PageLabel', to='db.Label'), + model_name="page", + name="labels", + field=models.ManyToManyField( + blank=True, + related_name="pages", + through="db.PageLabel", + to="db.Label", + ), ), ] diff --git a/apiserver/plane/db/migrations/0026_alter_projectmember_view_props.py b/apiserver/plane/db/migrations/0026_alter_projectmember_view_props.py index 6f74fa499..310087f97 100644 --- a/apiserver/plane/db/migrations/0026_alter_projectmember_view_props.py +++ b/apiserver/plane/db/migrations/0026_alter_projectmember_view_props.py @@ -5,15 +5,16 @@ import plane.db.models.project class Migration(migrations.Migration): - dependencies = [ - ('db', '0025_auto_20230331_0203'), + ("db", "0025_auto_20230331_0203"), ] operations = [ migrations.AlterField( - model_name='projectmember', - name='view_props', - field=models.JSONField(default=plane.db.models.project.get_default_props), + model_name="projectmember", + name="view_props", + field=models.JSONField( + default=plane.db.models.project.get_default_props + ), ), - ] \ No newline at end of file + ] diff --git a/apiserver/plane/db/migrations/0027_auto_20230409_0312.py b/apiserver/plane/db/migrations/0027_auto_20230409_0312.py index 8d344cf34..0377c84e8 100644 --- a/apiserver/plane/db/migrations/0027_auto_20230409_0312.py +++ b/apiserver/plane/db/migrations/0027_auto_20230409_0312.py @@ -9,89 +9,289 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0026_alter_projectmember_view_props'), + ("db", "0026_alter_projectmember_view_props"), ] operations = [ migrations.CreateModel( - name='Estimate', + name="Estimate", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255)), - ('description', models.TextField(blank=True, verbose_name='Estimate Description')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimate_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_estimate', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimate_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_estimate', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ( + "description", + models.TextField( + blank=True, verbose_name="Estimate Description" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="estimate_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_estimate", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="estimate_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_estimate", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Estimate', - 'verbose_name_plural': 'Estimates', - 'db_table': 'estimates', - 'ordering': ('name',), - 'unique_together': {('name', 'project')}, + "verbose_name": "Estimate", + "verbose_name_plural": "Estimates", + "db_table": "estimates", + "ordering": ("name",), + "unique_together": {("name", "project")}, }, ), migrations.RemoveField( - model_name='issue', - name='attachments', + model_name="issue", + name="attachments", ), migrations.AddField( - model_name='issue', - name='estimate_point', - field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(7)]), + model_name="issue", + name="estimate_point", + field=models.IntegerField( + default=0, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(7), + ], + ), ), migrations.CreateModel( - name='IssueAttachment', + name="IssueAttachment", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('attributes', models.JSONField(default=dict)), - ('asset', models.FileField(upload_to=plane.db.models.issue.get_upload_path, validators=[plane.db.models.issue.file_size])), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueattachment_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_attachment', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issueattachment', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issueattachment_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_issueattachment', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("attributes", models.JSONField(default=dict)), + ( + "asset", + models.FileField( + upload_to=plane.db.models.issue.get_upload_path, + validators=[plane.db.models.issue.file_size], + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueattachment_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_attachment", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_issueattachment", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issueattachment_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_issueattachment", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Issue Attachment', - 'verbose_name_plural': 'Issue Attachments', - 'db_table': 'issue_attachments', - 'ordering': ('-created_at',), + "verbose_name": "Issue Attachment", + "verbose_name_plural": "Issue Attachments", + "db_table": "issue_attachments", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='project', - name='estimate', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='db.estimate'), + model_name="project", + name="estimate", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="projects", + to="db.estimate", + ), ), migrations.CreateModel( - name='EstimatePoint', + name="EstimatePoint", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('key', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(7)])), - ('description', models.TextField(blank=True)), - ('value', models.CharField(max_length=20)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimatepoint_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('estimate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='points', to='db.estimate')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_estimatepoint', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='estimatepoint_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_estimatepoint', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "key", + models.IntegerField( + default=0, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(7), + ], + ), + ), + ("description", models.TextField(blank=True)), + ("value", models.CharField(max_length=20)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="estimatepoint_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "estimate", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="points", + to="db.estimate", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_estimatepoint", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="estimatepoint_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_estimatepoint", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Estimate Point', - 'verbose_name_plural': 'Estimate Points', - 'db_table': 'estimate_points', - 'ordering': ('value',), - 'unique_together': {('value', 'estimate')}, + "verbose_name": "Estimate Point", + "verbose_name_plural": "Estimate Points", + "db_table": "estimate_points", + "ordering": ("value",), + "unique_together": {("value", "estimate")}, }, ), ] diff --git a/apiserver/plane/db/migrations/0028_auto_20230414_1703.py b/apiserver/plane/db/migrations/0028_auto_20230414_1703.py index bb0b67b92..ffccccff5 100644 --- a/apiserver/plane/db/migrations/0028_auto_20230414_1703.py +++ b/apiserver/plane/db/migrations/0028_auto_20230414_1703.py @@ -8,41 +8,99 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0027_auto_20230409_0312'), + ("db", "0027_auto_20230409_0312"), ] operations = [ migrations.AddField( - model_name='user', - name='theme', + model_name="user", + name="theme", field=models.JSONField(default=dict), ), migrations.AlterField( - model_name='issue', - name='estimate_point', - field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(7)]), + model_name="issue", + name="estimate_point", + field=models.IntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(7), + ], + ), ), migrations.CreateModel( - name='WorkspaceTheme', + name="WorkspaceTheme", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=300)), - ('colors', models.JSONField(default=dict)), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='themes', to=settings.AUTH_USER_MODEL)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacetheme_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workspacetheme_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='themes', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=300)), + ("colors", models.JSONField(default=dict)), + ( + "actor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="themes", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspacetheme_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="workspacetheme_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="themes", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Workspace Theme', - 'verbose_name_plural': 'Workspace Themes', - 'db_table': 'workspace_themes', - 'ordering': ('-created_at',), - 'unique_together': {('workspace', 'name')}, + "verbose_name": "Workspace Theme", + "verbose_name_plural": "Workspace Themes", + "db_table": "workspace_themes", + "ordering": ("-created_at",), + "unique_together": {("workspace", "name")}, }, ), ] diff --git a/apiserver/plane/db/migrations/0029_auto_20230502_0126.py b/apiserver/plane/db/migrations/0029_auto_20230502_0126.py index 373cc39bd..cd2b1b865 100644 --- a/apiserver/plane/db/migrations/0029_auto_20230502_0126.py +++ b/apiserver/plane/db/migrations/0029_auto_20230502_0126.py @@ -7,52 +7,110 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0028_auto_20230414_1703'), + ("db", "0028_auto_20230414_1703"), ] operations = [ migrations.AddField( - model_name='cycle', - name='view_props', + model_name="cycle", + name="view_props", field=models.JSONField(default=dict), ), migrations.AddField( - model_name='importer', - name='imported_data', + model_name="importer", + name="imported_data", field=models.JSONField(null=True), ), migrations.AddField( - model_name='module', - name='view_props', + model_name="module", + name="view_props", field=models.JSONField(default=dict), ), migrations.CreateModel( - name='SlackProjectSync', + name="SlackProjectSync", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('access_token', models.CharField(max_length=300)), - ('scopes', models.TextField()), - ('bot_user_id', models.CharField(max_length=50)), - ('webhook_url', models.URLField(max_length=1000)), - ('data', models.JSONField(default=dict)), - ('team_id', models.CharField(max_length=30)), - ('team_name', models.CharField(max_length=300)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_slackprojectsync', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slackprojectsync_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_slackprojectsync', to='db.workspace')), - ('workspace_integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slack_syncs', to='db.workspaceintegration')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("access_token", models.CharField(max_length=300)), + ("scopes", models.TextField()), + ("bot_user_id", models.CharField(max_length=50)), + ("webhook_url", models.URLField(max_length=1000)), + ("data", models.JSONField(default=dict)), + ("team_id", models.CharField(max_length=30)), + ("team_name", models.CharField(max_length=300)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="slackprojectsync_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_slackprojectsync", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="slackprojectsync_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_slackprojectsync", + to="db.workspace", + ), + ), + ( + "workspace_integration", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="slack_syncs", + to="db.workspaceintegration", + ), + ), ], options={ - 'verbose_name': 'Slack Project Sync', - 'verbose_name_plural': 'Slack Project Syncs', - 'db_table': 'slack_project_syncs', - 'ordering': ('-created_at',), - 'unique_together': {('team_id', 'project')}, + "verbose_name": "Slack Project Sync", + "verbose_name_plural": "Slack Project Syncs", + "db_table": "slack_project_syncs", + "ordering": ("-created_at",), + "unique_together": {("team_id", "project")}, }, ), ] diff --git a/apiserver/plane/db/migrations/0030_alter_estimatepoint_unique_together.py b/apiserver/plane/db/migrations/0030_alter_estimatepoint_unique_together.py index bfc1da530..63db205dc 100644 --- a/apiserver/plane/db/migrations/0030_alter_estimatepoint_unique_together.py +++ b/apiserver/plane/db/migrations/0030_alter_estimatepoint_unique_together.py @@ -4,14 +4,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('db', '0029_auto_20230502_0126'), + ("db", "0029_auto_20230502_0126"), ] operations = [ migrations.AlterUniqueTogether( - name='estimatepoint', + name="estimatepoint", unique_together=set(), ), ] diff --git a/apiserver/plane/db/migrations/0031_analyticview.py b/apiserver/plane/db/migrations/0031_analyticview.py index 7e02b78b2..f4520a8f5 100644 --- a/apiserver/plane/db/migrations/0031_analyticview.py +++ b/apiserver/plane/db/migrations/0031_analyticview.py @@ -7,31 +7,75 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0030_alter_estimatepoint_unique_together'), + ("db", "0030_alter_estimatepoint_unique_together"), ] operations = [ migrations.CreateModel( - name='AnalyticView', + name="AnalyticView", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255)), - ('description', models.TextField(blank=True)), - ('query', models.JSONField()), - ('query_dict', models.JSONField(default=dict)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='analyticview_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='analyticview_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='analytics', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True)), + ("query", models.JSONField()), + ("query_dict", models.JSONField(default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="analyticview_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="analyticview_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="analytics", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Analytic', - 'verbose_name_plural': 'Analytics', - 'db_table': 'analytic_views', - 'ordering': ('-created_at',), + "verbose_name": "Analytic", + "verbose_name_plural": "Analytics", + "db_table": "analytic_views", + "ordering": ("-created_at",), }, ), ] diff --git a/apiserver/plane/db/migrations/0032_auto_20230520_2015.py b/apiserver/plane/db/migrations/0032_auto_20230520_2015.py index 27c13537e..c781d298c 100644 --- a/apiserver/plane/db/migrations/0032_auto_20230520_2015.py +++ b/apiserver/plane/db/migrations/0032_auto_20230520_2015.py @@ -4,20 +4,19 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0031_analyticview'), + ("db", "0031_analyticview"), ] operations = [ migrations.RenameField( - model_name='project', - old_name='icon', - new_name='emoji', + model_name="project", + old_name="icon", + new_name="emoji", ), migrations.AddField( - model_name='project', - name='icon_prop', + model_name="project", + name="icon_prop", field=models.JSONField(null=True), ), ] diff --git a/apiserver/plane/db/migrations/0033_auto_20230618_2125.py b/apiserver/plane/db/migrations/0033_auto_20230618_2125.py index 8eb2eda62..1705aead6 100644 --- a/apiserver/plane/db/migrations/0033_auto_20230618_2125.py +++ b/apiserver/plane/db/migrations/0033_auto_20230618_2125.py @@ -7,77 +7,210 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0032_auto_20230520_2015'), + ("db", "0032_auto_20230520_2015"), ] operations = [ migrations.CreateModel( - name='Inbox', + name="Inbox", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255)), - ('description', models.TextField(blank=True, verbose_name='Inbox Description')), - ('is_default', models.BooleanField(default=False)), - ('view_props', models.JSONField(default=dict)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inbox_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ( + "description", + models.TextField( + blank=True, verbose_name="Inbox Description" + ), + ), + ("is_default", models.BooleanField(default=False)), + ("view_props", models.JSONField(default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="inbox_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), ], options={ - 'verbose_name': 'Inbox', - 'verbose_name_plural': 'Inboxes', - 'db_table': 'inboxes', - 'ordering': ('name',), + "verbose_name": "Inbox", + "verbose_name_plural": "Inboxes", + "db_table": "inboxes", + "ordering": ("name",), }, ), migrations.AddField( - model_name='project', - name='inbox_view', + model_name="project", + name="inbox_view", field=models.BooleanField(default=False), ), migrations.CreateModel( - name='InboxIssue', + name="InboxIssue", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('status', models.IntegerField(choices=[(-2, 'Pending'), (-1, 'Rejected'), (0, 'Snoozed'), (1, 'Accepted'), (2, 'Duplicate')], default=-2)), - ('snoozed_till', models.DateTimeField(null=True)), - ('source', models.TextField(blank=True, null=True)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inboxissue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('duplicate_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inbox_duplicate', to='db.issue')), - ('inbox', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_inbox', to='db.inbox')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_inbox', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_inboxissue', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inboxissue_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_inboxissue', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "status", + models.IntegerField( + choices=[ + (-2, "Pending"), + (-1, "Rejected"), + (0, "Snoozed"), + (1, "Accepted"), + (2, "Duplicate"), + ], + default=-2, + ), + ), + ("snoozed_till", models.DateTimeField(null=True)), + ("source", models.TextField(blank=True, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="inboxissue_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "duplicate_to", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="inbox_duplicate", + to="db.issue", + ), + ), + ( + "inbox", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_inbox", + to="db.inbox", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_inbox", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_inboxissue", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="inboxissue_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_inboxissue", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'InboxIssue', - 'verbose_name_plural': 'InboxIssues', - 'db_table': 'inbox_issues', - 'ordering': ('-created_at',), + "verbose_name": "InboxIssue", + "verbose_name_plural": "InboxIssues", + "db_table": "inbox_issues", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='inbox', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_inbox', to='db.project'), + model_name="inbox", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_inbox", + to="db.project", + ), ), migrations.AddField( - model_name='inbox', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inbox_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + model_name="inbox", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="inbox_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), ), migrations.AddField( - model_name='inbox', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_inbox', to='db.workspace'), + model_name="inbox", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_inbox", + to="db.workspace", + ), ), migrations.AlterUniqueTogether( - name='inbox', - unique_together={('name', 'project')}, + name="inbox", + unique_together={("name", "project")}, ), ] diff --git a/apiserver/plane/db/migrations/0034_auto_20230628_1046.py b/apiserver/plane/db/migrations/0034_auto_20230628_1046.py index cdd722f59..dd6d21f6d 100644 --- a/apiserver/plane/db/migrations/0034_auto_20230628_1046.py +++ b/apiserver/plane/db/migrations/0034_auto_20230628_1046.py @@ -4,36 +4,35 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('db', '0033_auto_20230618_2125'), + ("db", "0033_auto_20230618_2125"), ] operations = [ migrations.RemoveField( - model_name='timelineissue', - name='created_by', + model_name="timelineissue", + name="created_by", ), migrations.RemoveField( - model_name='timelineissue', - name='issue', + model_name="timelineissue", + name="issue", ), migrations.RemoveField( - model_name='timelineissue', - name='project', + model_name="timelineissue", + name="project", ), migrations.RemoveField( - model_name='timelineissue', - name='updated_by', + model_name="timelineissue", + name="updated_by", ), migrations.RemoveField( - model_name='timelineissue', - name='workspace', + model_name="timelineissue", + name="workspace", ), migrations.DeleteModel( - name='Shortcut', + name="Shortcut", ), migrations.DeleteModel( - name='TimelineIssue', + name="TimelineIssue", ), ] diff --git a/apiserver/plane/db/migrations/0035_auto_20230704_2225.py b/apiserver/plane/db/migrations/0035_auto_20230704_2225.py index dec6265e6..806bfef51 100644 --- a/apiserver/plane/db/migrations/0035_auto_20230704_2225.py +++ b/apiserver/plane/db/migrations/0035_auto_20230704_2225.py @@ -10,7 +10,9 @@ def update_company_organization_size(apps, schema_editor): obj.organization_size = str(obj.company_size) updated_size.append(obj) - Model.objects.bulk_update(updated_size, ["organization_size"], batch_size=100) + Model.objects.bulk_update( + updated_size, ["organization_size"], batch_size=100 + ) class Migration(migrations.Migration): @@ -28,7 +30,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="workspace", name="name", - field=models.CharField(max_length=80, verbose_name="Workspace Name"), + field=models.CharField( + max_length=80, verbose_name="Workspace Name" + ), ), migrations.AlterField( model_name="workspace", diff --git a/apiserver/plane/db/migrations/0036_alter_workspace_organization_size.py b/apiserver/plane/db/migrations/0036_alter_workspace_organization_size.py index 0b182f50b..86748c778 100644 --- a/apiserver/plane/db/migrations/0036_alter_workspace_organization_size.py +++ b/apiserver/plane/db/migrations/0036_alter_workspace_organization_size.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0035_auto_20230704_2225'), + ("db", "0035_auto_20230704_2225"), ] operations = [ migrations.AlterField( - model_name='workspace', - name='organization_size', + model_name="workspace", + name="organization_size", field=models.CharField(max_length=20), ), ] diff --git a/apiserver/plane/db/migrations/0037_issue_archived_at_project_archive_in_and_more.py b/apiserver/plane/db/migrations/0037_issue_archived_at_project_archive_in_and_more.py index d11e1afd8..e659133d1 100644 --- a/apiserver/plane/db/migrations/0037_issue_archived_at_project_archive_in_and_more.py +++ b/apiserver/plane/db/migrations/0037_issue_archived_at_project_archive_in_and_more.py @@ -8,7 +8,6 @@ import plane.db.models.user import uuid - def onboarding_default_steps(apps, schema_editor): default_onboarding_schema = { "workspace_join": True, @@ -24,7 +23,9 @@ def onboarding_default_steps(apps, schema_editor): obj.is_tour_completed = True updated_user.append(obj) - Model.objects.bulk_update(updated_user, ["onboarding_step", "is_tour_completed"], batch_size=100) + Model.objects.bulk_update( + updated_user, ["onboarding_step", "is_tour_completed"], batch_size=100 + ) class Migration(migrations.Migration): @@ -78,7 +79,9 @@ class Migration(migrations.Migration): migrations.AddField( model_name="user", name="onboarding_step", - field=models.JSONField(default=plane.db.models.user.get_default_onboarding), + field=models.JSONField( + default=plane.db.models.user.get_default_onboarding + ), ), migrations.RunPython(onboarding_default_steps), migrations.CreateModel( @@ -86,7 +89,9 @@ class Migration(migrations.Migration): fields=[ ( "created_at", - models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), ), ( "updated_at", @@ -110,7 +115,10 @@ class Migration(migrations.Migration): ("entity_name", models.CharField(max_length=255)), ("title", models.TextField()), ("message", models.JSONField(null=True)), - ("message_html", models.TextField(blank=True, default="

")), + ( + "message_html", + models.TextField(blank=True, default="

"), + ), ("message_stripped", models.TextField(blank=True, null=True)), ("sender", models.CharField(max_length=255)), ("read_at", models.DateTimeField(null=True)), @@ -183,7 +191,9 @@ class Migration(migrations.Migration): fields=[ ( "created_at", - models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), ), ( "updated_at", diff --git a/apiserver/plane/db/migrations/0038_auto_20230720_1505.py b/apiserver/plane/db/migrations/0038_auto_20230720_1505.py index 1f5c63a89..53e50ed41 100644 --- a/apiserver/plane/db/migrations/0038_auto_20230720_1505.py +++ b/apiserver/plane/db/migrations/0038_auto_20230720_1505.py @@ -15,14 +15,12 @@ def restructure_theming(apps, schema_editor): "text": current_theme.get("textBase", ""), "sidebarText": current_theme.get("textBase", ""), "palette": f"""{current_theme.get("bgBase","")},{current_theme.get("textBase", "")},{current_theme.get("accent", "")},{current_theme.get("sidebar","")},{current_theme.get("textBase", "")}""", - "darkPalette": current_theme.get("darkPalette", "") + "darkPalette": current_theme.get("darkPalette", ""), } obj.theme = updated_theme updated_user.append(obj) - Model.objects.bulk_update( - updated_user, ["theme"], batch_size=100 - ) + Model.objects.bulk_update(updated_user, ["theme"], batch_size=100) class Migration(migrations.Migration): @@ -30,6 +28,4 @@ class Migration(migrations.Migration): ("db", "0037_issue_archived_at_project_archive_in_and_more"), ] - operations = [ - migrations.RunPython(restructure_theming) - ] + operations = [migrations.RunPython(restructure_theming)] diff --git a/apiserver/plane/db/migrations/0039_auto_20230723_2203.py b/apiserver/plane/db/migrations/0039_auto_20230723_2203.py index 5d5747543..26849d7f7 100644 --- a/apiserver/plane/db/migrations/0039_auto_20230723_2203.py +++ b/apiserver/plane/db/migrations/0039_auto_20230723_2203.py @@ -55,7 +55,9 @@ def update_workspace_member_props(apps, schema_editor): updated_workspace_member.append(obj) - Model.objects.bulk_update(updated_workspace_member, ["view_props"], batch_size=100) + Model.objects.bulk_update( + updated_workspace_member, ["view_props"], batch_size=100 + ) def update_project_member_sort_order(apps, schema_editor): @@ -67,7 +69,9 @@ def update_project_member_sort_order(apps, schema_editor): obj.sort_order = random.randint(1, 65536) updated_project_members.append(obj) - Model.objects.bulk_update(updated_project_members, ["sort_order"], batch_size=100) + Model.objects.bulk_update( + updated_project_members, ["sort_order"], batch_size=100 + ) class Migration(migrations.Migration): @@ -79,18 +83,22 @@ class Migration(migrations.Migration): migrations.RunPython(rename_field), migrations.RunPython(update_workspace_member_props), migrations.AlterField( - model_name='workspacemember', - name='view_props', - field=models.JSONField(default=plane.db.models.workspace.get_default_props), + model_name="workspacemember", + name="view_props", + field=models.JSONField( + default=plane.db.models.workspace.get_default_props + ), ), migrations.AddField( - model_name='workspacemember', - name='default_props', - field=models.JSONField(default=plane.db.models.workspace.get_default_props), + model_name="workspacemember", + name="default_props", + field=models.JSONField( + default=plane.db.models.workspace.get_default_props + ), ), migrations.AddField( - model_name='projectmember', - name='sort_order', + model_name="projectmember", + name="sort_order", field=models.FloatField(default=65535), ), migrations.RunPython(update_project_member_sort_order), diff --git a/apiserver/plane/db/migrations/0040_projectmember_preferences_user_cover_image_and_more.py b/apiserver/plane/db/migrations/0040_projectmember_preferences_user_cover_image_and_more.py index 5662ef666..76f8e6272 100644 --- a/apiserver/plane/db/migrations/0040_projectmember_preferences_user_cover_image_and_more.py +++ b/apiserver/plane/db/migrations/0040_projectmember_preferences_user_cover_image_and_more.py @@ -8,74 +8,209 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0039_auto_20230723_2203'), + ("db", "0039_auto_20230723_2203"), ] operations = [ migrations.AddField( - model_name='projectmember', - name='preferences', - field=models.JSONField(default=plane.db.models.project.get_default_preferences), + model_name="projectmember", + name="preferences", + field=models.JSONField( + default=plane.db.models.project.get_default_preferences + ), ), migrations.AddField( - model_name='user', - name='cover_image', + model_name="user", + name="cover_image", field=models.URLField(blank=True, max_length=800, null=True), ), migrations.CreateModel( - name='IssueReaction', + name="IssueReaction", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('reaction', models.CharField(max_length=20)), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_reactions', to=settings.AUTH_USER_MODEL)), - ('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='issue_reactions', to='db.issue')), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("reaction", models.CharField(max_length=20)), + ( + "actor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_reactions", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "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="issue_reactions", + to="db.issue", + ), + ), + ( + "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': 'Issue Reaction', - 'verbose_name_plural': 'Issue Reactions', - 'db_table': 'issue_reactions', - 'ordering': ('-created_at',), - 'unique_together': {('issue', 'actor', 'reaction')}, + "verbose_name": "Issue Reaction", + "verbose_name_plural": "Issue Reactions", + "db_table": "issue_reactions", + "ordering": ("-created_at",), + "unique_together": {("issue", "actor", "reaction")}, }, ), migrations.CreateModel( - name='CommentReaction', + name="CommentReaction", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('reaction', models.CharField(max_length=20)), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comment_reactions', to=settings.AUTH_USER_MODEL)), - ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comment_reactions', to='db.issuecomment')), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("reaction", models.CharField(max_length=20)), + ( + "actor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comment_reactions", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "comment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comment_reactions", + to="db.issuecomment", + ), + ), + ( + "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': 'Comment Reaction', - 'verbose_name_plural': 'Comment Reactions', - 'db_table': 'comment_reactions', - 'ordering': ('-created_at',), - 'unique_together': {('comment', 'actor', 'reaction')}, + "verbose_name": "Comment Reaction", + "verbose_name_plural": "Comment Reactions", + "db_table": "comment_reactions", + "ordering": ("-created_at",), + "unique_together": {("comment", "actor", "reaction")}, }, - ), - migrations.AlterField( - model_name='project', - name='identifier', - field=models.CharField(max_length=12, verbose_name='Project Identifier'), ), migrations.AlterField( - model_name='projectidentifier', - name='name', + model_name="project", + name="identifier", + field=models.CharField( + max_length=12, verbose_name="Project Identifier" + ), + ), + migrations.AlterField( + model_name="projectidentifier", + name="name", field=models.CharField(max_length=12), ), ] diff --git a/apiserver/plane/db/migrations/0041_cycle_sort_order_issuecomment_access_and_more.py b/apiserver/plane/db/migrations/0041_cycle_sort_order_issuecomment_access_and_more.py index 07c302c76..91119dbbd 100644 --- a/apiserver/plane/db/migrations/0041_cycle_sort_order_issuecomment_access_and_more.py +++ b/apiserver/plane/db/migrations/0041_cycle_sort_order_issuecomment_access_and_more.py @@ -10,6 +10,7 @@ import uuid import random import string + def generate_display_name(apps, schema_editor): UserModel = apps.get_model("db", "User") updated_users = [] @@ -20,7 +21,9 @@ def generate_display_name(apps, schema_editor): else "".join(random.choice(string.ascii_letters) for _ in range(6)) ) updated_users.append(obj) - UserModel.objects.bulk_update(updated_users, ["display_name"], batch_size=100) + UserModel.objects.bulk_update( + updated_users, ["display_name"], batch_size=100 + ) def rectify_field_issue_activity(apps, schema_editor): @@ -72,7 +75,13 @@ def update_assignee_issue_activity(apps, schema_editor): Model.objects.bulk_update( updated_activity, - ["old_value", "new_value", "old_identifier", "new_identifier", "comment"], + [ + "old_value", + "new_value", + "old_identifier", + "new_identifier", + "comment", + ], batch_size=200, ) @@ -93,7 +102,9 @@ def random_cycle_order(apps, schema_editor): for obj in CycleModel.objects.all(): obj.sort_order = random.randint(1, 65536) updated_cycles.append(obj) - CycleModel.objects.bulk_update(updated_cycles, ["sort_order"], batch_size=100) + CycleModel.objects.bulk_update( + updated_cycles, ["sort_order"], batch_size=100 + ) def random_module_order(apps, schema_editor): @@ -102,7 +113,9 @@ def random_module_order(apps, schema_editor): for obj in ModuleModel.objects.all(): obj.sort_order = random.randint(1, 65536) updated_modules.append(obj) - ModuleModel.objects.bulk_update(updated_modules, ["sort_order"], batch_size=100) + ModuleModel.objects.bulk_update( + updated_modules, ["sort_order"], batch_size=100 + ) def update_user_issue_properties(apps, schema_editor): @@ -125,111 +138,353 @@ def workspace_member_properties(apps, schema_editor): updated_workspace_members.append(obj) WorkspaceMemberModel.objects.bulk_update( - updated_workspace_members, ["view_props", "default_props"], batch_size=100 + updated_workspace_members, + ["view_props", "default_props"], + batch_size=100, ) -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('db', '0040_projectmember_preferences_user_cover_image_and_more'), + ("db", "0040_projectmember_preferences_user_cover_image_and_more"), ] operations = [ migrations.AddField( - model_name='cycle', - name='sort_order', + model_name="cycle", + name="sort_order", field=models.FloatField(default=65535), ), migrations.AddField( - model_name='issuecomment', - name='access', - field=models.CharField(choices=[('INTERNAL', 'INTERNAL'), ('EXTERNAL', 'EXTERNAL')], default='INTERNAL', max_length=100), + model_name="issuecomment", + name="access", + field=models.CharField( + choices=[("INTERNAL", "INTERNAL"), ("EXTERNAL", "EXTERNAL")], + default="INTERNAL", + max_length=100, + ), ), migrations.AddField( - model_name='module', - name='sort_order', + model_name="module", + name="sort_order", field=models.FloatField(default=65535), ), migrations.AddField( - model_name='user', - name='display_name', - field=models.CharField(default='', max_length=255), + model_name="user", + name="display_name", + field=models.CharField(default="", max_length=255), ), migrations.CreateModel( - name='ExporterHistory', + name="ExporterHistory", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('project', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(default=uuid.uuid4), blank=True, null=True, size=None)), - ('provider', models.CharField(choices=[('json', 'json'), ('csv', 'csv'), ('xlsx', 'xlsx')], max_length=50)), - ('status', models.CharField(choices=[('queued', 'Queued'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='queued', max_length=50)), - ('reason', models.TextField(blank=True)), - ('key', models.TextField(blank=True)), - ('url', models.URLField(blank=True, max_length=800, null=True)), - ('token', models.CharField(default=plane.db.models.exporter.generate_token, max_length=255, unique=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')), - ('initiated_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_exporters', to=settings.AUTH_USER_MODEL)), - ('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_exporters', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "project", + django.contrib.postgres.fields.ArrayField( + base_field=models.UUIDField(default=uuid.uuid4), + blank=True, + null=True, + size=None, + ), + ), + ( + "provider", + models.CharField( + choices=[ + ("json", "json"), + ("csv", "csv"), + ("xlsx", "xlsx"), + ], + max_length=50, + ), + ), + ( + "status", + models.CharField( + choices=[ + ("queued", "Queued"), + ("processing", "Processing"), + ("completed", "Completed"), + ("failed", "Failed"), + ], + default="queued", + max_length=50, + ), + ), + ("reason", models.TextField(blank=True)), + ("key", models.TextField(blank=True)), + ( + "url", + models.URLField(blank=True, max_length=800, null=True), + ), + ( + "token", + models.CharField( + default=plane.db.models.exporter.generate_token, + max_length=255, + unique=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", + ), + ), + ( + "initiated_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_exporters", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "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_exporters", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Exporter', - 'verbose_name_plural': 'Exporters', - 'db_table': 'exporters', - 'ordering': ('-created_at',), + "verbose_name": "Exporter", + "verbose_name_plural": "Exporters", + "db_table": "exporters", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='ProjectDeployBoard', + name="ProjectDeployBoard", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('anchor', models.CharField(db_index=True, default=plane.db.models.project.get_anchor, max_length=255, unique=True)), - ('comments', models.BooleanField(default=False)), - ('reactions', models.BooleanField(default=False)), - ('votes', models.BooleanField(default=False)), - ('views', models.JSONField(default=plane.db.models.project.get_default_views)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('inbox', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bord_inbox', to='db.inbox')), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "anchor", + models.CharField( + db_index=True, + default=plane.db.models.project.get_anchor, + max_length=255, + unique=True, + ), + ), + ("comments", models.BooleanField(default=False)), + ("reactions", models.BooleanField(default=False)), + ("votes", models.BooleanField(default=False)), + ( + "views", + models.JSONField( + default=plane.db.models.project.get_default_views + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "inbox", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="bord_inbox", + to="db.inbox", + ), + ), + ( + "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 Deploy Board', - 'verbose_name_plural': 'Project Deploy Boards', - 'db_table': 'project_deploy_boards', - 'ordering': ('-created_at',), - 'unique_together': {('project', 'anchor')}, + "verbose_name": "Project Deploy Board", + "verbose_name_plural": "Project Deploy Boards", + "db_table": "project_deploy_boards", + "ordering": ("-created_at",), + "unique_together": {("project", "anchor")}, }, ), migrations.CreateModel( - name='IssueVote', + name="IssueVote", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('vote', models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')])), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to=settings.AUTH_USER_MODEL)), - ('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='votes', to='db.issue')), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "vote", + models.IntegerField( + choices=[(-1, "DOWNVOTE"), (1, "UPVOTE")] + ), + ), + ( + "actor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="votes", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "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="votes", + to="db.issue", + ), + ), + ( + "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': 'Issue Vote', - 'verbose_name_plural': 'Issue Votes', - 'db_table': 'issue_votes', - 'ordering': ('-created_at',), - 'unique_together': {('issue', 'actor')}, + "verbose_name": "Issue Vote", + "verbose_name_plural": "Issue Votes", + "db_table": "issue_votes", + "ordering": ("-created_at",), + "unique_together": {("issue", "actor")}, }, ), migrations.AlterField( - model_name='modulelink', - name='title', + model_name="modulelink", + name="title", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.RunPython(generate_display_name), diff --git a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py index 01af46d20..f1fa99a36 100644 --- a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py +++ b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py @@ -5,56 +5,762 @@ from django.db import migrations, models import django.db.models.deletion import uuid + def update_user_timezones(apps, schema_editor): UserModel = apps.get_model("db", "User") updated_users = [] for obj in UserModel.objects.all(): obj.user_timezone = "UTC" updated_users.append(obj) - UserModel.objects.bulk_update(updated_users, ["user_timezone"], batch_size=100) + UserModel.objects.bulk_update( + updated_users, ["user_timezone"], batch_size=100 + ) class Migration(migrations.Migration): - dependencies = [ - ('db', '0041_cycle_sort_order_issuecomment_access_and_more'), + ("db", "0041_cycle_sort_order_issuecomment_access_and_more"), ] operations = [ migrations.AlterField( - model_name='user', - name='user_timezone', - field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'America/Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fort_Wayne', 'America/Fort_Wayne'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Knox_IN', 'America/Knox_IN'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Rosario', 'America/Rosario'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/South_Pole', 'Antarctica/South_Pole'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Ashkhabad', 'Asia/Ashkhabad'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Chungking', 'Asia/Chungking'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Dacca', 'Asia/Dacca'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macao', 'Asia/Macao'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimbu', 'Asia/Thimbu'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ujung_Pandang', 'Asia/Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Ulan_Bator', 'Asia/Ulan_Bator'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/ACT', 'Australia/ACT'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/LHI', 'Australia/LHI'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/NSW', 'Australia/NSW'), ('Australia/North', 'Australia/North'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Queensland', 'Australia/Queensland'), ('Australia/South', 'Australia/South'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Tasmania', 'Australia/Tasmania'), ('Australia/Victoria', 'Australia/Victoria'), ('Australia/West', 'Australia/West'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Brazil/Acre', 'Brazil/Acre'), ('Brazil/DeNoronha', 'Brazil/DeNoronha'), ('Brazil/East', 'Brazil/East'), ('Brazil/West', 'Brazil/West'), ('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Canada/Saskatchewan', 'Canada/Saskatchewan'), ('Canada/Yukon', 'Canada/Yukon'), ('Chile/Continental', 'Chile/Continental'), ('Chile/EasterIsland', 'Chile/EasterIsland'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('GMT', 'GMT'), ('GMT+0', 'GMT+0'), ('GMT-0', 'GMT-0'), ('GMT0', 'GMT0'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('Mexico/BajaNorte', 'Mexico/BajaNorte'), ('Mexico/BajaSur', 'Mexico/BajaSur'), ('Mexico/General', 'Mexico/General'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('US/Alaska', 'US/Alaska'), ('US/Aleutian', 'US/Aleutian'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/East-Indiana', 'US/East-Indiana'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Indiana-Starke', 'US/Indiana-Starke'), ('US/Michigan', 'US/Michigan'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('US/Samoa', 'US/Samoa'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')], default='UTC', max_length=255), + model_name="user", + name="user_timezone", + field=models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ( + "America/Argentina/Buenos_Aires", + "America/Argentina/Buenos_Aires", + ), + ( + "America/Argentina/Catamarca", + "America/Argentina/Catamarca", + ), + ( + "America/Argentina/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ( + "America/Argentina/La_Rioja", + "America/Argentina/La_Rioja", + ), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ( + "America/Argentina/Rio_Gallegos", + "America/Argentina/Rio_Gallegos", + ), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ( + "America/Argentina/San_Juan", + "America/Argentina/San_Juan", + ), + ( + "America/Argentina/San_Luis", + "America/Argentina/San_Luis", + ), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ( + "America/Indiana/Indianapolis", + "America/Indiana/Indianapolis", + ), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ( + "America/Indiana/Petersburg", + "America/Indiana/Petersburg", + ), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ( + "America/Kentucky/Louisville", + "America/Kentucky/Louisville", + ), + ( + "America/Kentucky/Monticello", + "America/Kentucky/Monticello", + ), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ( + "America/North_Dakota/Beulah", + "America/North_Dakota/Beulah", + ), + ( + "America/North_Dakota/Center", + "America/North_Dakota/Center", + ), + ( + "America/North_Dakota/New_Salem", + "America/North_Dakota/New_Salem", + ), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Kyiv", "Europe/Kyiv"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default="UTC", + max_length=255, + ), ), migrations.AlterField( - model_name='issuelink', - name='title', + model_name="issuelink", + name="title", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.RunPython(update_user_timezones), migrations.AlterField( - model_name='issuevote', - name='vote', - field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1), + model_name="issuevote", + name="vote", + field=models.IntegerField( + choices=[(-1, "DOWNVOTE"), (1, "UPVOTE")], default=1 + ), ), migrations.CreateModel( - name='ProjectPublicMember', + name="ProjectPublicMember", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('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')), - ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='public_project_members', to=settings.AUTH_USER_MODEL)), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "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", + ), + ), + ( + "member", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="public_project_members", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "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 Public Member', - 'verbose_name_plural': 'Project Public Members', - 'db_table': 'project_public_members', - 'ordering': ('-created_at',), - 'unique_together': {('project', 'member')}, + "verbose_name": "Project Public Member", + "verbose_name_plural": "Project Public Members", + "db_table": "project_public_members", + "ordering": ("-created_at",), + "unique_together": {("project", "member")}, }, ), ] diff --git a/apiserver/plane/db/migrations/0043_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0043_alter_analyticview_created_by_and_more.py index 5a806c704..81d91bb78 100644 --- a/apiserver/plane/db/migrations/0043_alter_analyticview_created_by_and_more.py +++ b/apiserver/plane/db/migrations/0043_alter_analyticview_created_by_and_more.py @@ -24,7 +24,9 @@ def create_issue_relation(apps, schema_editor): updated_by_id=blocked_issue.updated_by_id, ) ) - IssueRelation.objects.bulk_create(updated_issue_relation, batch_size=100) + IssueRelation.objects.bulk_create( + updated_issue_relation, batch_size=100 + ) except Exception as e: print(e) capture_exception(e) @@ -36,47 +38,137 @@ def update_issue_priority_choice(apps, schema_editor): for obj in IssueModel.objects.filter(priority=None): obj.priority = "none" updated_issues.append(obj) - IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100) + IssueModel.objects.bulk_update( + updated_issues, ["priority"], batch_size=100 + ) class Migration(migrations.Migration): - dependencies = [ - ('db', '0042_alter_analyticview_created_by_and_more'), + ("db", "0042_alter_analyticview_created_by_and_more"), ] operations = [ migrations.CreateModel( - name='IssueRelation', + name="IssueRelation", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('relation_type', models.CharField(choices=[('duplicate', 'Duplicate'), ('relates_to', 'Relates To'), ('blocked_by', 'Blocked By')], default='blocked_by', max_length=20, verbose_name='Issue Relation Type')), - ('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='issue_relation', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), - ('related_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_related', to='db.issue')), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "relation_type", + models.CharField( + choices=[ + ("duplicate", "Duplicate"), + ("relates_to", "Relates To"), + ("blocked_by", "Blocked By"), + ], + default="blocked_by", + max_length=20, + verbose_name="Issue Relation Type", + ), + ), + ( + "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="issue_relation", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "related_issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_related", + to="db.issue", + ), + ), + ( + "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 Relation', - 'verbose_name_plural': 'Issue Relations', - 'db_table': 'issue_relations', - 'ordering': ('-created_at',), - 'unique_together': {('issue', 'related_issue')}, + "verbose_name": "Issue Relation", + "verbose_name_plural": "Issue Relations", + "db_table": "issue_relations", + "ordering": ("-created_at",), + "unique_together": {("issue", "related_issue")}, }, ), migrations.AddField( - model_name='issue', - name='is_draft', + model_name="issue", + name="is_draft", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='issue', - name='priority', - field=models.CharField(choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low'), ('none', 'None')], default='none', max_length=30, verbose_name='Issue Priority'), + model_name="issue", + name="priority", + field=models.CharField( + choices=[ + ("urgent", "Urgent"), + ("high", "High"), + ("medium", "Medium"), + ("low", "Low"), + ("none", "None"), + ], + default="none", + max_length=30, + verbose_name="Issue Priority", + ), ), migrations.RunPython(create_issue_relation), migrations.RunPython(update_issue_priority_choice), diff --git a/apiserver/plane/db/migrations/0044_auto_20230913_0709.py b/apiserver/plane/db/migrations/0044_auto_20230913_0709.py index 19a1449af..d42b3431e 100644 --- a/apiserver/plane/db/migrations/0044_auto_20230913_0709.py +++ b/apiserver/plane/db/migrations/0044_auto_20230913_0709.py @@ -8,12 +8,16 @@ def workspace_member_props(old_props): "filters": { "priority": old_props.get("filters", {}).get("priority", None), "state": old_props.get("filters", {}).get("state", None), - "state_group": old_props.get("filters", {}).get("state_group", None), + "state_group": old_props.get("filters", {}).get( + "state_group", None + ), "assignees": old_props.get("filters", {}).get("assignees", None), "created_by": old_props.get("filters", {}).get("created_by", None), "labels": old_props.get("filters", {}).get("labels", None), "start_date": old_props.get("filters", {}).get("start_date", None), - "target_date": old_props.get("filters", {}).get("target_date", None), + "target_date": old_props.get("filters", {}).get( + "target_date", None + ), "subscriber": old_props.get("filters", {}).get("subscriber", None), }, "display_filters": { @@ -27,18 +31,28 @@ def workspace_member_props(old_props): }, "display_properties": { "assignee": old_props.get("properties", {}).get("assignee", True), - "attachment_count": old_props.get("properties", {}).get("attachment_count", True), - "created_on": old_props.get("properties", {}).get("created_on", True), + "attachment_count": old_props.get("properties", {}).get( + "attachment_count", True + ), + "created_on": old_props.get("properties", {}).get( + "created_on", True + ), "due_date": old_props.get("properties", {}).get("due_date", True), "estimate": old_props.get("properties", {}).get("estimate", True), "key": old_props.get("properties", {}).get("key", True), "labels": old_props.get("properties", {}).get("labels", True), "link": old_props.get("properties", {}).get("link", True), "priority": old_props.get("properties", {}).get("priority", True), - "start_date": old_props.get("properties", {}).get("start_date", True), + "start_date": old_props.get("properties", {}).get( + "start_date", True + ), "state": old_props.get("properties", {}).get("state", True), - "sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True), - "updated_on": old_props.get("properties", {}).get("updated_on", True), + "sub_issue_count": old_props.get("properties", {}).get( + "sub_issue_count", True + ), + "updated_on": old_props.get("properties", {}).get( + "updated_on", True + ), }, } return new_props @@ -49,12 +63,16 @@ def project_member_props(old_props): "filters": { "priority": old_props.get("filters", {}).get("priority", None), "state": old_props.get("filters", {}).get("state", None), - "state_group": old_props.get("filters", {}).get("state_group", None), + "state_group": old_props.get("filters", {}).get( + "state_group", None + ), "assignees": old_props.get("filters", {}).get("assignees", None), "created_by": old_props.get("filters", {}).get("created_by", None), "labels": old_props.get("filters", {}).get("labels", None), "start_date": old_props.get("filters", {}).get("start_date", None), - "target_date": old_props.get("filters", {}).get("target_date", None), + "target_date": old_props.get("filters", {}).get( + "target_date", None + ), "subscriber": old_props.get("filters", {}).get("subscriber", None), }, "display_filters": { @@ -75,59 +93,75 @@ def cycle_module_props(old_props): "filters": { "priority": old_props.get("filters", {}).get("priority", None), "state": old_props.get("filters", {}).get("state", None), - "state_group": old_props.get("filters", {}).get("state_group", None), + "state_group": old_props.get("filters", {}).get( + "state_group", None + ), "assignees": old_props.get("filters", {}).get("assignees", None), "created_by": old_props.get("filters", {}).get("created_by", None), "labels": old_props.get("filters", {}).get("labels", None), "start_date": old_props.get("filters", {}).get("start_date", None), - "target_date": old_props.get("filters", {}).get("target_date", None), + "target_date": old_props.get("filters", {}).get( + "target_date", None + ), "subscriber": old_props.get("filters", {}).get("subscriber", None), }, } return new_props - + def update_workspace_member_view_props(apps, schema_editor): WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember") updated_workspace_member = [] for obj in WorkspaceMemberModel.objects.all(): - obj.view_props = workspace_member_props(obj.view_props) - obj.default_props = workspace_member_props(obj.default_props) - updated_workspace_member.append(obj) - WorkspaceMemberModel.objects.bulk_update(updated_workspace_member, ["view_props", "default_props"], batch_size=100) + obj.view_props = workspace_member_props(obj.view_props) + obj.default_props = workspace_member_props(obj.default_props) + updated_workspace_member.append(obj) + WorkspaceMemberModel.objects.bulk_update( + updated_workspace_member, + ["view_props", "default_props"], + batch_size=100, + ) + def update_project_member_view_props(apps, schema_editor): ProjectMemberModel = apps.get_model("db", "ProjectMember") updated_project_member = [] for obj in ProjectMemberModel.objects.all(): - obj.view_props = project_member_props(obj.view_props) - obj.default_props = project_member_props(obj.default_props) - updated_project_member.append(obj) - ProjectMemberModel.objects.bulk_update(updated_project_member, ["view_props", "default_props"], batch_size=100) + obj.view_props = project_member_props(obj.view_props) + obj.default_props = project_member_props(obj.default_props) + updated_project_member.append(obj) + ProjectMemberModel.objects.bulk_update( + updated_project_member, ["view_props", "default_props"], batch_size=100 + ) + def update_cycle_props(apps, schema_editor): CycleModel = apps.get_model("db", "Cycle") updated_cycle = [] for obj in CycleModel.objects.all(): - if "filter" in obj.view_props: - obj.view_props = cycle_module_props(obj.view_props) - updated_cycle.append(obj) - CycleModel.objects.bulk_update(updated_cycle, ["view_props"], batch_size=100) + if "filter" in obj.view_props: + obj.view_props = cycle_module_props(obj.view_props) + updated_cycle.append(obj) + CycleModel.objects.bulk_update( + updated_cycle, ["view_props"], batch_size=100 + ) + def update_module_props(apps, schema_editor): ModuleModel = apps.get_model("db", "Module") updated_module = [] for obj in ModuleModel.objects.all(): - if "filter" in obj.view_props: - obj.view_props = cycle_module_props(obj.view_props) - updated_module.append(obj) - ModuleModel.objects.bulk_update(updated_module, ["view_props"], batch_size=100) + if "filter" in obj.view_props: + obj.view_props = cycle_module_props(obj.view_props) + updated_module.append(obj) + ModuleModel.objects.bulk_update( + updated_module, ["view_props"], batch_size=100 + ) class Migration(migrations.Migration): - dependencies = [ - ('db', '0043_alter_analyticview_created_by_and_more'), + ("db", "0043_alter_analyticview_created_by_and_more"), ] operations = [ diff --git a/apiserver/plane/db/migrations/0045_issueactivity_epoch_workspacemember_issue_props_and_more.py b/apiserver/plane/db/migrations/0045_issueactivity_epoch_workspacemember_issue_props_and_more.py index 4b9c1b1eb..9ac528829 100644 --- a/apiserver/plane/db/migrations/0045_issueactivity_epoch_workspacemember_issue_props_and_more.py +++ b/apiserver/plane/db/migrations/0045_issueactivity_epoch_workspacemember_issue_props_and_more.py @@ -21,6 +21,7 @@ def update_issue_activity_priority(apps, schema_editor): batch_size=2000, ) + def update_issue_activity_blocked(apps, schema_editor): IssueActivity = apps.get_model("db", "IssueActivity") updated_issue_activity = [] @@ -34,44 +35,104 @@ def update_issue_activity_blocked(apps, schema_editor): batch_size=1000, ) -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('db', '0044_auto_20230913_0709'), + ("db", "0044_auto_20230913_0709"), ] operations = [ migrations.CreateModel( - name='GlobalView', + name="GlobalView", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255, verbose_name='View Name')), - ('description', models.TextField(blank=True, verbose_name='View Description')), - ('query', models.JSONField(verbose_name='View Query')), - ('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)), - ('query_data', models.JSONField(default=dict)), - ('sort_order', models.FloatField(default=65535)), - ('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')), - ('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='global_views', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "name", + models.CharField(max_length=255, verbose_name="View Name"), + ), + ( + "description", + models.TextField( + blank=True, verbose_name="View Description" + ), + ), + ("query", models.JSONField(verbose_name="View Query")), + ( + "access", + models.PositiveSmallIntegerField( + choices=[(0, "Private"), (1, "Public")], default=1 + ), + ), + ("query_data", models.JSONField(default=dict)), + ("sort_order", models.FloatField(default=65535)), + ( + "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", + ), + ), + ( + "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="global_views", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Global View', - 'verbose_name_plural': 'Global Views', - 'db_table': 'global_views', - 'ordering': ('-created_at',), + "verbose_name": "Global View", + "verbose_name_plural": "Global Views", + "db_table": "global_views", + "ordering": ("-created_at",), }, ), migrations.AddField( - model_name='workspacemember', - name='issue_props', - field=models.JSONField(default=plane.db.models.workspace.get_issue_props), + model_name="workspacemember", + name="issue_props", + field=models.JSONField( + default=plane.db.models.workspace.get_issue_props + ), ), migrations.AddField( - model_name='issueactivity', - name='epoch', + model_name="issueactivity", + name="epoch", field=models.FloatField(null=True), ), migrations.RunPython(update_issue_activity_priority), diff --git a/apiserver/plane/db/migrations/0046_label_sort_order_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0046_label_sort_order_alter_analyticview_created_by_and_more.py index f02660e1d..be58c8f5f 100644 --- a/apiserver/plane/db/migrations/0046_label_sort_order_alter_analyticview_created_by_and_more.py +++ b/apiserver/plane/db/migrations/0046_label_sort_order_alter_analyticview_created_by_and_more.py @@ -7,977 +7,2001 @@ import plane.db.models.issue import uuid import random + def random_sort_ordering(apps, schema_editor): Label = apps.get_model("db", "Label") bulk_labels = [] for label in Label.objects.all(): - label.sort_order = random.randint(0,65535) + label.sort_order = random.randint(0, 65535) bulk_labels.append(label) Label.objects.bulk_update(bulk_labels, ["sort_order"], batch_size=1000) -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'), + ( + "db", + "0045_issueactivity_epoch_workspacemember_issue_props_and_more", + ), ] operations = [ migrations.AddField( - model_name='label', - name='sort_order', + model_name="label", + name="sort_order", field=models.FloatField(default=65535), ), migrations.AlterField( - model_name='analyticview', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='analyticview', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='apitoken', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='apitoken', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='cycle', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='cycle', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='cycle', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='cycle', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='cyclefavorite', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='cyclefavorite', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='cyclefavorite', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='cyclefavorite', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='cycleissue', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='cycleissue', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='cycleissue', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='cycleissue', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='estimate', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='estimate', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='estimate', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='estimate', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='estimatepoint', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='estimatepoint', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='estimatepoint', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='estimatepoint', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='fileasset', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='fileasset', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='githubcommentsync', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='githubcommentsync', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='githubcommentsync', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='githubcommentsync', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='githubissuesync', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='githubissuesync', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='githubissuesync', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='githubissuesync', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='githubrepository', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='githubrepository', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='githubrepository', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='githubrepository', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='githubrepositorysync', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='githubrepositorysync', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='githubrepositorysync', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='githubrepositorysync', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='importer', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='importer', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='importer', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='importer', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='inbox', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='inbox', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='inbox', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='inbox', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='inboxissue', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='inboxissue', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='inboxissue', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='inboxissue', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='integration', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='integration', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issue', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issue', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issue', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issue', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issueactivity', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issueactivity', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issueactivity', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issueactivity', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issueassignee', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issueassignee', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issueassignee', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issueassignee', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issueattachment', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issueattachment', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issueattachment', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issueattachment', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issueblocker', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issueblocker', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issueblocker', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issueblocker', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issuecomment', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issuecomment', - name='issue', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_comments', to='db.issue'), - ), - migrations.AlterField( - model_name='issuecomment', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issuecomment', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issuecomment', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issuelabel', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issuelabel', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issuelabel', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issuelabel', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issuelink', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issuelink', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issuelink', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issuelink', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issueproperty', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issueproperty', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issueproperty', - name='properties', - field=models.JSONField(default=plane.db.models.issue.get_default_properties), - ), - migrations.AlterField( - model_name='issueproperty', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issueproperty', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issuesequence', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issuesequence', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issuesequence', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issuesequence', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issueview', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issueview', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issueview', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issueview', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='issueviewfavorite', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='issueviewfavorite', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='issueviewfavorite', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='issueviewfavorite', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='label', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='label', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='label', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='label', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='module', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='module', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='module', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='module', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='modulefavorite', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='modulefavorite', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='modulefavorite', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='modulefavorite', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='moduleissue', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='moduleissue', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='moduleissue', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='moduleissue', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='modulelink', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='modulelink', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='modulelink', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='modulelink', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='modulemember', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='modulemember', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='modulemember', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='modulemember', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='page', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='page', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='page', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='page', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='pageblock', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='pageblock', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='pageblock', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='pageblock', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='pagefavorite', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='pagefavorite', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='pagefavorite', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='pagefavorite', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='pagelabel', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='pagelabel', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='pagelabel', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='pagelabel', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='project', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='project', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='projectfavorite', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='projectfavorite', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='projectfavorite', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='projectfavorite', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='projectidentifier', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='projectidentifier', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='projectmember', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='projectmember', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='projectmember', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='projectmember', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='projectmemberinvite', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='projectmemberinvite', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='projectmemberinvite', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='projectmemberinvite', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='slackprojectsync', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='slackprojectsync', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='slackprojectsync', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='slackprojectsync', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='socialloginconnection', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='socialloginconnection', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='state', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='state', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), - ), - migrations.AlterField( - model_name='state', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='state', - name='workspace', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'), - ), - migrations.AlterField( - model_name='team', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='team', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='teammember', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='teammember', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='workspace', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='workspace', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='workspaceintegration', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='workspaceintegration', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='workspacemember', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='workspacemember', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='workspacememberinvite', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='workspacememberinvite', - name='updated_by', - field=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'), - ), - migrations.AlterField( - model_name='workspacetheme', - name='created_by', - field=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'), - ), - migrations.AlterField( - model_name='workspacetheme', - name='updated_by', - field=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'), + model_name="analyticview", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="analyticview", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="apitoken", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="apitoken", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="cycle", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="cycle", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="cycle", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="cycle", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="cyclefavorite", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="cyclefavorite", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="cyclefavorite", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="cyclefavorite", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="cycleissue", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="cycleissue", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="cycleissue", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="cycleissue", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="estimate", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="estimate", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="estimate", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="estimate", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="estimatepoint", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="estimatepoint", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="estimatepoint", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="estimatepoint", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="fileasset", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="fileasset", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="githubcommentsync", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="githubcommentsync", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="githubcommentsync", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="githubcommentsync", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="githubissuesync", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="githubissuesync", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="githubissuesync", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="githubissuesync", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="githubrepository", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="githubrepository", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="githubrepository", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="githubrepository", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="githubrepositorysync", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="githubrepositorysync", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="githubrepositorysync", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="githubrepositorysync", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="importer", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="importer", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="importer", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="importer", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="inbox", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="inbox", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="inbox", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="inbox", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="inboxissue", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="inboxissue", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="inboxissue", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="inboxissue", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="integration", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="integration", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issue", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issue", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issue", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issue", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issueactivity", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueactivity", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issueactivity", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueactivity", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issueassignee", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueassignee", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issueassignee", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueassignee", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issueattachment", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueattachment", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issueattachment", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueattachment", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issueblocker", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueblocker", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issueblocker", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueblocker", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issuecomment", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issuecomment", + name="issue", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_comments", + to="db.issue", + ), + ), + migrations.AlterField( + model_name="issuecomment", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issuecomment", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issuecomment", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issuelabel", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issuelabel", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issuelabel", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issuelabel", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issuelink", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issuelink", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issuelink", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issuelink", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issueproperty", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueproperty", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issueproperty", + name="properties", + field=models.JSONField( + default=plane.db.models.issue.get_default_properties + ), + ), + migrations.AlterField( + model_name="issueproperty", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueproperty", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issuesequence", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issuesequence", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issuesequence", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issuesequence", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issueview", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueview", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issueview", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueview", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="issueviewfavorite", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueviewfavorite", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="issueviewfavorite", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="issueviewfavorite", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="label", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="label", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="label", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="label", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="module", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="module", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="module", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="module", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="modulefavorite", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="modulefavorite", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="modulefavorite", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="modulefavorite", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="moduleissue", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="moduleissue", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="moduleissue", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="moduleissue", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="modulelink", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="modulelink", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="modulelink", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="modulelink", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="modulemember", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="modulemember", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="modulemember", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="modulemember", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="page", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="page", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="page", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="page", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="pageblock", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="pageblock", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="pageblock", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="pageblock", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="pagefavorite", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="pagefavorite", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="pagefavorite", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="pagefavorite", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="pagelabel", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="pagelabel", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="pagelabel", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="pagelabel", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="project", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="project", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="projectfavorite", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="projectfavorite", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="projectfavorite", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="projectfavorite", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="projectidentifier", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="projectidentifier", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="projectmember", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="projectmember", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="projectmember", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="projectmember", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="projectmemberinvite", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="projectmemberinvite", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="projectmemberinvite", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="projectmemberinvite", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="slackprojectsync", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="slackprojectsync", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="slackprojectsync", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="slackprojectsync", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="socialloginconnection", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="socialloginconnection", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="state", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="state", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AlterField( + model_name="state", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="state", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AlterField( + model_name="team", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="team", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="teammember", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="teammember", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspace", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspace", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspaceintegration", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspaceintegration", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspacemember", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspacemember", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspacememberinvite", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspacememberinvite", + name="updated_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspacetheme", + name="created_by", + field=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", + ), + ), + migrations.AlterField( + model_name="workspacetheme", + name="updated_by", + field=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", + ), ), migrations.CreateModel( - name='IssueMention', + name="IssueMention", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('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='issue_mention', to='db.issue')), - ('mention', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to=settings.AUTH_USER_MODEL)), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "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="issue_mention", + to="db.issue", + ), + ), + ( + "mention", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_mention", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "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': 'Issue Mention', - 'verbose_name_plural': 'Issue Mentions', - 'db_table': 'issue_mentions', - 'ordering': ('-created_at',), - 'unique_together': {('issue', 'mention')}, + "verbose_name": "Issue Mention", + "verbose_name_plural": "Issue Mentions", + "db_table": "issue_mentions", + "ordering": ("-created_at",), + "unique_together": {("issue", "mention")}, }, ), migrations.RunPython(random_sort_ordering), diff --git a/apiserver/plane/db/migrations/0047_webhook_apitoken_description_apitoken_expired_at_and_more.py b/apiserver/plane/db/migrations/0047_webhook_apitoken_description_apitoken_expired_at_and_more.py index d44f760d0..f0a52a355 100644 --- a/apiserver/plane/db/migrations/0047_webhook_apitoken_description_apitoken_expired_at_and_more.py +++ b/apiserver/plane/db/migrations/0047_webhook_apitoken_description_apitoken_expired_at_and_more.py @@ -9,123 +9,288 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0046_label_sort_order_alter_analyticview_created_by_and_more'), + ("db", "0046_label_sort_order_alter_analyticview_created_by_and_more"), ] operations = [ migrations.CreateModel( - name='Webhook', + name="Webhook", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('url', models.URLField(validators=[plane.db.models.webhook.validate_schema, plane.db.models.webhook.validate_domain])), - ('is_active', models.BooleanField(default=True)), - ('secret_key', models.CharField(default=plane.db.models.webhook.generate_token, max_length=255)), - ('project', models.BooleanField(default=False)), - ('issue', models.BooleanField(default=False)), - ('module', models.BooleanField(default=False)), - ('cycle', models.BooleanField(default=False)), - ('issue_comment', models.BooleanField(default=False)), - ('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')), - ('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_webhooks', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "url", + models.URLField( + validators=[ + plane.db.models.webhook.validate_schema, + plane.db.models.webhook.validate_domain, + ] + ), + ), + ("is_active", models.BooleanField(default=True)), + ( + "secret_key", + models.CharField( + default=plane.db.models.webhook.generate_token, + max_length=255, + ), + ), + ("project", models.BooleanField(default=False)), + ("issue", models.BooleanField(default=False)), + ("module", models.BooleanField(default=False)), + ("cycle", models.BooleanField(default=False)), + ("issue_comment", models.BooleanField(default=False)), + ( + "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", + ), + ), + ( + "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_webhooks", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Webhook', - 'verbose_name_plural': 'Webhooks', - 'db_table': 'webhooks', - 'ordering': ('-created_at',), - 'unique_together': {('workspace', 'url')}, + "verbose_name": "Webhook", + "verbose_name_plural": "Webhooks", + "db_table": "webhooks", + "ordering": ("-created_at",), + "unique_together": {("workspace", "url")}, }, ), migrations.AddField( - model_name='apitoken', - name='description', + model_name="apitoken", + name="description", field=models.TextField(blank=True), ), migrations.AddField( - model_name='apitoken', - name='expired_at', + model_name="apitoken", + name="expired_at", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='apitoken', - name='is_active', + model_name="apitoken", + name="is_active", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='apitoken', - name='last_used', + model_name="apitoken", + name="last_used", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='projectmember', - name='is_active', + model_name="projectmember", + name="is_active", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='workspacemember', - name='is_active', + model_name="workspacemember", + name="is_active", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='apitoken', - name='token', - field=models.CharField(db_index=True, default=plane.db.models.api.generate_token, max_length=255, unique=True), + model_name="apitoken", + name="token", + field=models.CharField( + db_index=True, + default=plane.db.models.api.generate_token, + max_length=255, + unique=True, + ), ), migrations.CreateModel( - name='WebhookLog', + name="WebhookLog", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('event_type', models.CharField(blank=True, max_length=255, null=True)), - ('request_method', models.CharField(blank=True, max_length=10, null=True)), - ('request_headers', models.TextField(blank=True, null=True)), - ('request_body', models.TextField(blank=True, null=True)), - ('response_status', models.TextField(blank=True, null=True)), - ('response_headers', models.TextField(blank=True, null=True)), - ('response_body', models.TextField(blank=True, null=True)), - ('retry_count', models.PositiveSmallIntegerField(default=0)), - ('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')), - ('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')), - ('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='db.webhook')), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhook_logs', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "event_type", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "request_method", + models.CharField(blank=True, max_length=10, null=True), + ), + ("request_headers", models.TextField(blank=True, null=True)), + ("request_body", models.TextField(blank=True, null=True)), + ("response_status", models.TextField(blank=True, null=True)), + ("response_headers", models.TextField(blank=True, null=True)), + ("response_body", models.TextField(blank=True, null=True)), + ("retry_count", models.PositiveSmallIntegerField(default=0)), + ( + "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", + ), + ), + ( + "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", + ), + ), + ( + "webhook", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="logs", + to="db.webhook", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="webhook_logs", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Webhook Log', - 'verbose_name_plural': 'Webhook Logs', - 'db_table': 'webhook_logs', - 'ordering': ('-created_at',), + "verbose_name": "Webhook Log", + "verbose_name_plural": "Webhook Logs", + "db_table": "webhook_logs", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='APIActivityLog', + name="APIActivityLog", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('token_identifier', models.CharField(max_length=255)), - ('path', models.CharField(max_length=255)), - ('method', models.CharField(max_length=10)), - ('query_params', models.TextField(blank=True, null=True)), - ('headers', models.TextField(blank=True, null=True)), - ('body', models.TextField(blank=True, null=True)), - ('response_code', models.PositiveIntegerField()), - ('response_body', models.TextField(blank=True, null=True)), - ('ip_address', models.GenericIPAddressField(blank=True, null=True)), - ('user_agent', models.CharField(blank=True, max_length=512, null=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')), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("token_identifier", models.CharField(max_length=255)), + ("path", models.CharField(max_length=255)), + ("method", models.CharField(max_length=10)), + ("query_params", models.TextField(blank=True, null=True)), + ("headers", models.TextField(blank=True, null=True)), + ("body", models.TextField(blank=True, null=True)), + ("response_code", models.PositiveIntegerField()), + ("response_body", models.TextField(blank=True, null=True)), + ( + "ip_address", + models.GenericIPAddressField(blank=True, null=True), + ), + ( + "user_agent", + models.CharField(blank=True, max_length=512, null=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", + ), + ), + ( + "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': 'API Activity Log', - 'verbose_name_plural': 'API Activity Logs', - 'db_table': 'api_activity_logs', - 'ordering': ('-created_at',), + "verbose_name": "API Activity Log", + "verbose_name_plural": "API Activity Logs", + "db_table": "api_activity_logs", + "ordering": ("-created_at",), }, ), ] diff --git a/apiserver/plane/db/migrations/0048_auto_20231116_0713.py b/apiserver/plane/db/migrations/0048_auto_20231116_0713.py index 8d896b01d..791affed6 100644 --- a/apiserver/plane/db/migrations/0048_auto_20231116_0713.py +++ b/apiserver/plane/db/migrations/0048_auto_20231116_0713.py @@ -7,48 +7,135 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0047_webhook_apitoken_description_apitoken_expired_at_and_more'), + ( + "db", + "0047_webhook_apitoken_description_apitoken_expired_at_and_more", + ), ] operations = [ migrations.CreateModel( - name='PageLog', + name="PageLog", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('transaction', models.UUIDField(default=uuid.uuid4)), - ('entity_identifier', models.UUIDField(null=True)), - ('entity_name', models.CharField(choices=[('to_do', 'To Do'), ('issue', 'issue'), ('image', 'Image'), ('video', 'Video'), ('file', 'File'), ('link', 'Link'), ('cycle', 'Cycle'), ('module', 'Module'), ('back_link', 'Back Link'), ('forward_link', 'Forward Link'), ('page_mention', 'Page Mention'), ('user_mention', 'User Mention')], max_length=30, verbose_name='Transaction Type')), - ('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')), - ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_log', to='db.page')), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("transaction", models.UUIDField(default=uuid.uuid4)), + ("entity_identifier", models.UUIDField(null=True)), + ( + "entity_name", + models.CharField( + choices=[ + ("to_do", "To Do"), + ("issue", "issue"), + ("image", "Image"), + ("video", "Video"), + ("file", "File"), + ("link", "Link"), + ("cycle", "Cycle"), + ("module", "Module"), + ("back_link", "Back Link"), + ("forward_link", "Forward Link"), + ("page_mention", "Page Mention"), + ("user_mention", "User Mention"), + ], + max_length=30, + verbose_name="Transaction Type", + ), + ), + ( + "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", + ), + ), + ( + "page", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="page_log", + to="db.page", + ), + ), + ( + "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': 'Page Log', - 'verbose_name_plural': 'Page Logs', - 'db_table': 'page_logs', - 'ordering': ('-created_at',), - 'unique_together': {('page', 'transaction')} + "verbose_name": "Page Log", + "verbose_name_plural": "Page Logs", + "db_table": "page_logs", + "ordering": ("-created_at",), + "unique_together": {("page", "transaction")}, }, ), migrations.AddField( - model_name='page', - name='archived_at', + model_name="page", + name="archived_at", field=models.DateField(null=True), ), migrations.AddField( - model_name='page', - name='is_locked', + model_name="page", + name="is_locked", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='page', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_page', to='db.page'), + model_name="page", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="child_page", + to="db.page", + ), ), - ] \ No newline at end of file + ] diff --git a/apiserver/plane/db/migrations/0049_auto_20231116_0713.py b/apiserver/plane/db/migrations/0049_auto_20231116_0713.py index 75d5e5982..d59fc5a84 100644 --- a/apiserver/plane/db/migrations/0049_auto_20231116_0713.py +++ b/apiserver/plane/db/migrations/0049_auto_20231116_0713.py @@ -18,7 +18,9 @@ def update_pages(apps, schema_editor): # looping through all the pages for page in Page.objects.all(): page_blocks = PageBlock.objects.filter( - page_id=page.id, project_id=page.project_id, workspace_id=page.workspace_id + page_id=page.id, + project_id=page.project_id, + workspace_id=page.workspace_id, ).order_by("sort_order") if page_blocks: @@ -69,4 +71,4 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(update_pages), - ] \ No newline at end of file + ] diff --git a/apiserver/plane/db/migrations/0050_user_use_case_alter_workspace_organization_size.py b/apiserver/plane/db/migrations/0050_user_use_case_alter_workspace_organization_size.py index a8807d104..327a5ab72 100644 --- a/apiserver/plane/db/migrations/0050_user_use_case_alter_workspace_organization_size.py +++ b/apiserver/plane/db/migrations/0050_user_use_case_alter_workspace_organization_size.py @@ -3,37 +3,41 @@ from django.db import migrations, models import plane.db.models.workspace + def user_password_autoset(apps, schema_editor): User = apps.get_model("db", "User") User.objects.update(is_password_autoset=True) class Migration(migrations.Migration): - dependencies = [ - ('db', '0049_auto_20231116_0713'), + ("db", "0049_auto_20231116_0713"), ] operations = [ migrations.AddField( - model_name='user', - name='use_case', + model_name="user", + name="use_case", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='workspace', - name='organization_size', + model_name="workspace", + name="organization_size", field=models.CharField(blank=True, max_length=20, null=True), ), migrations.AddField( - model_name='fileasset', - name='is_deleted', + model_name="fileasset", + name="is_deleted", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='workspace', - name='slug', - field=models.SlugField(max_length=48, unique=True, validators=[plane.db.models.workspace.slug_validator]), + model_name="workspace", + name="slug", + field=models.SlugField( + max_length=48, + unique=True, + validators=[plane.db.models.workspace.slug_validator], + ), ), - migrations.RunPython(user_password_autoset), + migrations.RunPython(user_password_autoset), ] diff --git a/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py b/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py index 19267dfc2..886cee52d 100644 --- a/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py +++ b/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py @@ -4,80 +4,79 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0050_user_use_case_alter_workspace_organization_size'), + ("db", "0050_user_use_case_alter_workspace_organization_size"), ] operations = [ migrations.AddField( - model_name='cycle', - name='external_id', + model_name="cycle", + name="external_id", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='cycle', - name='external_source', + model_name="cycle", + name="external_source", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='inboxissue', - name='external_id', + model_name="inboxissue", + name="external_id", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='inboxissue', - name='external_source', + model_name="inboxissue", + name="external_source", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='issue', - name='external_id', + model_name="issue", + name="external_id", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='issue', - name='external_source', + model_name="issue", + name="external_source", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='issuecomment', - name='external_id', + model_name="issuecomment", + name="external_id", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='issuecomment', - name='external_source', + model_name="issuecomment", + name="external_source", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='label', - name='external_id', + model_name="label", + name="external_id", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='label', - name='external_source', + model_name="label", + name="external_source", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='module', - name='external_id', + model_name="module", + name="external_id", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='module', - name='external_source', + model_name="module", + name="external_source", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='state', - name='external_id', + model_name="state", + name="external_id", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='state', - name='external_source', + model_name="state", + name="external_source", field=models.CharField(blank=True, max_length=255, null=True), ), ] diff --git a/apiserver/plane/db/migrations/0052_auto_20231220_1141.py b/apiserver/plane/db/migrations/0052_auto_20231220_1141.py index 5ec614ab4..da16fb9f6 100644 --- a/apiserver/plane/db/migrations/0052_auto_20231220_1141.py +++ b/apiserver/plane/db/migrations/0052_auto_20231220_1141.py @@ -12,125 +12,368 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0051_cycle_external_id_cycle_external_source_and_more'), + ("db", "0051_cycle_external_id_cycle_external_source_and_more"), ] operations = [ migrations.RenameField( - model_name='issueview', - old_name='query_data', - new_name='filters', + model_name="issueview", + old_name="query_data", + new_name="filters", ), migrations.RenameField( - model_name='issueproperty', - old_name='properties', - new_name='display_properties', + model_name="issueproperty", + old_name="properties", + new_name="display_properties", ), migrations.AlterField( - model_name='issueproperty', - name='display_properties', - field=models.JSONField(default=plane.db.models.issue.get_default_display_properties), + model_name="issueproperty", + name="display_properties", + field=models.JSONField( + default=plane.db.models.issue.get_default_display_properties + ), ), migrations.AddField( - model_name='issueproperty', - name='display_filters', - field=models.JSONField(default=plane.db.models.issue.get_default_display_filters), + model_name="issueproperty", + name="display_filters", + field=models.JSONField( + default=plane.db.models.issue.get_default_display_filters + ), ), migrations.AddField( - model_name='issueproperty', - name='filters', - field=models.JSONField(default=plane.db.models.issue.get_default_filters), + model_name="issueproperty", + name="filters", + field=models.JSONField( + default=plane.db.models.issue.get_default_filters + ), ), migrations.AddField( - model_name='issueview', - name='display_filters', - field=models.JSONField(default=plane.db.models.view.get_default_display_filters), + model_name="issueview", + name="display_filters", + field=models.JSONField( + default=plane.db.models.view.get_default_display_filters + ), ), migrations.AddField( - model_name='issueview', - name='display_properties', - field=models.JSONField(default=plane.db.models.view.get_default_display_properties), + model_name="issueview", + name="display_properties", + field=models.JSONField( + default=plane.db.models.view.get_default_display_properties + ), ), migrations.AddField( - model_name='issueview', - name='sort_order', + model_name="issueview", + name="sort_order", field=models.FloatField(default=65535), ), migrations.AlterField( - model_name='issueview', - name='project', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + model_name="issueview", + name="project", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), ), migrations.CreateModel( - name='WorkspaceUserProperties', + name="WorkspaceUserProperties", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('filters', models.JSONField(default=plane.db.models.workspace.get_default_filters)), - ('display_filters', models.JSONField(default=plane.db.models.workspace.get_default_display_filters)), - ('display_properties', models.JSONField(default=plane.db.models.workspace.get_default_display_properties)), - ('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')), - ('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')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to=settings.AUTH_USER_MODEL)), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "filters", + models.JSONField( + default=plane.db.models.workspace.get_default_filters + ), + ), + ( + "display_filters", + models.JSONField( + default=plane.db.models.workspace.get_default_display_filters + ), + ), + ( + "display_properties", + models.JSONField( + default=plane.db.models.workspace.get_default_display_properties + ), + ), + ( + "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", + ), + ), + ( + "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", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_user_properties", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_user_properties", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Workspace User Property', - 'verbose_name_plural': 'Workspace User Property', - 'db_table': 'Workspace_user_properties', - 'ordering': ('-created_at',), - 'unique_together': {('workspace', 'user')}, + "verbose_name": "Workspace User Property", + "verbose_name_plural": "Workspace User Property", + "db_table": "Workspace_user_properties", + "ordering": ("-created_at",), + "unique_together": {("workspace", "user")}, }, ), migrations.CreateModel( - name='ModuleUserProperties', + name="ModuleUserProperties", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('filters', models.JSONField(default=plane.db.models.module.get_default_filters)), - ('display_filters', models.JSONField(default=plane.db.models.module.get_default_display_filters)), - ('display_properties', models.JSONField(default=plane.db.models.module.get_default_display_properties)), - ('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')), - ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to='db.module')), - ('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')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to=settings.AUTH_USER_MODEL)), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "filters", + models.JSONField( + default=plane.db.models.module.get_default_filters + ), + ), + ( + "display_filters", + models.JSONField( + default=plane.db.models.module.get_default_display_filters + ), + ), + ( + "display_properties", + models.JSONField( + default=plane.db.models.module.get_default_display_properties + ), + ), + ( + "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", + ), + ), + ( + "module", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="module_user_properties", + to="db.module", + ), + ), + ( + "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", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="module_user_properties", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Module User Property', - 'verbose_name_plural': 'Module User Property', - 'db_table': 'module_user_properties', - 'ordering': ('-created_at',), - 'unique_together': {('module', 'user')}, + "verbose_name": "Module User Property", + "verbose_name_plural": "Module User Property", + "db_table": "module_user_properties", + "ordering": ("-created_at",), + "unique_together": {("module", "user")}, }, ), migrations.CreateModel( - name='CycleUserProperties', + name="CycleUserProperties", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('filters', models.JSONField(default=plane.db.models.cycle.get_default_filters)), - ('display_filters', models.JSONField(default=plane.db.models.cycle.get_default_display_filters)), - ('display_properties', models.JSONField(default=plane.db.models.cycle.get_default_display_properties)), - ('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')), - ('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to='db.cycle')), - ('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')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to=settings.AUTH_USER_MODEL)), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "filters", + models.JSONField( + default=plane.db.models.cycle.get_default_filters + ), + ), + ( + "display_filters", + models.JSONField( + default=plane.db.models.cycle.get_default_display_filters + ), + ), + ( + "display_properties", + models.JSONField( + default=plane.db.models.cycle.get_default_display_properties + ), + ), + ( + "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", + ), + ), + ( + "cycle", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="cycle_user_properties", + to="db.cycle", + ), + ), + ( + "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", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="cycle_user_properties", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), ], options={ - 'verbose_name': 'Cycle User Property', - 'verbose_name_plural': 'Cycle User Properties', - 'db_table': 'cycle_user_properties', - 'ordering': ('-created_at',), - 'unique_together': {('cycle', 'user')}, + "verbose_name": "Cycle User Property", + "verbose_name_plural": "Cycle User Properties", + "db_table": "cycle_user_properties", + "ordering": ("-created_at",), + "unique_together": {("cycle", "user")}, }, ), - ] \ No newline at end of file + ] diff --git a/apiserver/plane/db/migrations/0053_auto_20240102_1315.py b/apiserver/plane/db/migrations/0053_auto_20240102_1315.py index 798d0d7bb..32b5ad2d5 100644 --- a/apiserver/plane/db/migrations/0053_auto_20240102_1315.py +++ b/apiserver/plane/db/migrations/0053_auto_20240102_1315.py @@ -11,31 +11,46 @@ def workspace_user_properties(apps, schema_editor): updated_workspace_user_properties.append( WorkspaceUserProperties( user_id=workspace_members.member_id, - display_filters=workspace_members.view_props.get("display_filters"), - display_properties=workspace_members.view_props.get("display_properties"), + display_filters=workspace_members.view_props.get( + "display_filters" + ), + display_properties=workspace_members.view_props.get( + "display_properties" + ), workspace_id=workspace_members.workspace_id, ) ) - WorkspaceUserProperties.objects.bulk_create(updated_workspace_user_properties, batch_size=2000) + WorkspaceUserProperties.objects.bulk_create( + updated_workspace_user_properties, batch_size=2000 + ) def project_user_properties(apps, schema_editor): IssueProperty = apps.get_model("db", "IssueProperty") updated_issue_user_properties = [] for issue_property in IssueProperty.objects.all(): - project_member = ProjectMember.objects.filter(project_id=issue_property.project_id, member_id=issue_property.user_id).first() + project_member = ProjectMember.objects.filter( + project_id=issue_property.project_id, + member_id=issue_property.user_id, + ).first() if project_member: issue_property.filters = project_member.view_props.get("filters") - issue_property.display_filters = project_member.view_props.get("display_filters") + issue_property.display_filters = project_member.view_props.get( + "display_filters" + ) updated_issue_user_properties.append(issue_property) - IssueProperty.objects.bulk_update(updated_issue_user_properties, ["filters", "display_filters"], batch_size=2000) + IssueProperty.objects.bulk_update( + updated_issue_user_properties, + ["filters", "display_filters"], + batch_size=2000, + ) def issue_view(apps, schema_editor): GlobalView = apps.get_model("db", "GlobalView") updated_issue_views = [] - + for global_view in GlobalView.objects.all(): updated_issue_views.append( IssueView( @@ -52,10 +67,10 @@ def issue_view(apps, schema_editor): ) IssueView.objects.bulk_create(updated_issue_views, batch_size=100) -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('db', '0052_auto_20231220_1141'), + ("db", "0052_auto_20231220_1141"), ] operations = [ diff --git a/apiserver/plane/db/migrations/0054_dashboard_widget_dashboardwidget.py b/apiserver/plane/db/migrations/0054_dashboard_widget_dashboardwidget.py new file mode 100644 index 000000000..933c229a1 --- /dev/null +++ b/apiserver/plane/db/migrations/0054_dashboard_widget_dashboardwidget.py @@ -0,0 +1,77 @@ +# Generated by Django 4.2.7 on 2024-01-08 06:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0053_auto_20240102_1315'), + ] + + operations = [ + migrations.CreateModel( + name='Dashboard', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=255)), + ('description_html', models.TextField(blank=True, default='

')), + ('identifier', models.UUIDField(null=True)), + ('is_default', models.BooleanField(default=False)), + ('type_identifier', models.CharField(choices=[('workspace', 'Workspace'), ('project', 'Project'), ('home', 'Home'), ('team', 'Team'), ('user', 'User')], default='home', max_length=30, verbose_name='Dashboard Type')), + ('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')), + ('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dashboards', to=settings.AUTH_USER_MODEL)), + ('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': 'Dashboard', + 'verbose_name_plural': 'Dashboards', + 'db_table': 'dashboards', + 'ordering': ('-created_at',), + }, + ), + migrations.CreateModel( + name='Widget', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('key', models.CharField(max_length=255)), + ('filters', models.JSONField(default=dict)), + ], + options={ + 'verbose_name': 'Widget', + 'verbose_name_plural': 'Widgets', + 'db_table': 'widgets', + 'ordering': ('-created_at',), + }, + ), + migrations.CreateModel( + name='DashboardWidget', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('is_visible', models.BooleanField(default=True)), + ('sort_order', models.FloatField(default=65535)), + ('filters', models.JSONField(default=dict)), + ('properties', 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')), + ('dashboard', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dashboard_widgets', to='db.dashboard')), + ('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')), + ('widget', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dashboard_widgets', to='db.widget')), + ], + options={ + 'verbose_name': 'Dashboard Widget', + 'verbose_name_plural': 'Dashboard Widgets', + 'db_table': 'dashboard_widgets', + 'ordering': ('-created_at',), + 'unique_together': {('widget', 'dashboard')}, + }, + ), + ] diff --git a/apiserver/plane/db/migrations/0055_auto_20240108_0648.py b/apiserver/plane/db/migrations/0055_auto_20240108_0648.py new file mode 100644 index 000000000..e369c185d --- /dev/null +++ b/apiserver/plane/db/migrations/0055_auto_20240108_0648.py @@ -0,0 +1,97 @@ +# Generated by Django 4.2.7 on 2024-01-08 06:48 + +from django.db import migrations + + +def create_widgets(apps, schema_editor): + Widget = apps.get_model("db", "Widget") + widgets_list = [ + {"key": "overview_stats", "filters": {}}, + { + "key": "assigned_issues", + "filters": { + "duration": "this_week", + "tab": "upcoming", + }, + }, + { + "key": "created_issues", + "filters": { + "duration": "this_week", + "tab": "upcoming", + }, + }, + { + "key": "issues_by_state_groups", + "filters": { + "duration": "this_week", + }, + }, + { + "key": "issues_by_priority", + "filters": { + "duration": "this_week", + }, + }, + {"key": "recent_activity", "filters": {}}, + {"key": "recent_projects", "filters": {}}, + {"key": "recent_collaborators", "filters": {}}, + ] + Widget.objects.bulk_create( + [ + Widget( + key=widget["key"], + filters=widget["filters"], + ) + for widget in widgets_list + ], + batch_size=10, + ) + + +def create_dashboards(apps, schema_editor): + Dashboard = apps.get_model("db", "Dashboard") + User = apps.get_model("db", "User") + Dashboard.objects.bulk_create( + [ + Dashboard( + name="Home dashboard", + description_html="

", + identifier=None, + owned_by_id=user_id, + type_identifier="home", + is_default=True, + ) + for user_id in User.objects.values_list('id', flat=True) + ], + batch_size=2000, + ) + + +def create_dashboard_widgets(apps, schema_editor): + Widget = apps.get_model("db", "Widget") + Dashboard = apps.get_model("db", "Dashboard") + DashboardWidget = apps.get_model("db", "DashboardWidget") + + updated_dashboard_widget = [ + DashboardWidget( + widget_id=widget_id, + dashboard_id=dashboard_id, + ) + for widget_id in Widget.objects.values_list('id', flat=True) + for dashboard_id in Dashboard.objects.values_list('id', flat=True) + ] + + DashboardWidget.objects.bulk_create(updated_dashboard_widget, batch_size=2000) + + +class Migration(migrations.Migration): + dependencies = [ + ("db", "0054_dashboard_widget_dashboardwidget"), + ] + + operations = [ + migrations.RunPython(create_widgets), + migrations.RunPython(create_dashboards), + migrations.RunPython(create_dashboard_widgets), + ] diff --git a/apiserver/plane/db/mixins.py b/apiserver/plane/db/mixins.py index 728cb9933..263f9ab9a 100644 --- a/apiserver/plane/db/mixins.py +++ b/apiserver/plane/db/mixins.py @@ -13,7 +13,9 @@ class TimeAuditModel(models.Model): auto_now_add=True, verbose_name="Created At", ) - updated_at = models.DateTimeField(auto_now=True, verbose_name="Last Modified At") + updated_at = models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ) class Meta: abstract = True diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index b88ee8e46..3a07a33f3 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -54,7 +54,14 @@ from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties from .view import GlobalView, IssueView, IssueViewFavorite -from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite, ModuleUserProperties +from .module import ( + Module, + ModuleMember, + ModuleIssue, + ModuleLink, + ModuleFavorite, + ModuleUserProperties, +) from .api import APIToken, APIActivityLog @@ -83,3 +90,5 @@ from .notification import Notification from .exporter import ExporterHistory from .webhook import Webhook, WebhookLog + +from .dashboard import Dashboard, DashboardWidget, Widget \ No newline at end of file diff --git a/apiserver/plane/db/models/api.py b/apiserver/plane/db/models/api.py index 0fa1d4aba..78da81814 100644 --- a/apiserver/plane/db/models/api.py +++ b/apiserver/plane/db/models/api.py @@ -38,7 +38,10 @@ class APIToken(BaseModel): choices=((0, "Human"), (1, "Bot")), default=0 ) workspace = models.ForeignKey( - "db.Workspace", related_name="api_tokens", on_delete=models.CASCADE, null=True + "db.Workspace", + related_name="api_tokens", + on_delete=models.CASCADE, + null=True, ) expired_at = models.DateTimeField(blank=True, null=True) diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py index ab3c38d9c..713508613 100644 --- a/apiserver/plane/db/models/asset.py +++ b/apiserver/plane/db/models/asset.py @@ -34,7 +34,10 @@ class FileAsset(BaseModel): ], ) workspace = models.ForeignKey( - "db.Workspace", on_delete=models.CASCADE, null=True, related_name="assets" + "db.Workspace", + on_delete=models.CASCADE, + null=True, + related_name="assets", ) is_deleted = models.BooleanField(default=False) diff --git a/apiserver/plane/db/models/base.py b/apiserver/plane/db/models/base.py index d0531e881..63c08afa4 100644 --- a/apiserver/plane/db/models/base.py +++ b/apiserver/plane/db/models/base.py @@ -12,7 +12,11 @@ from ..mixins import AuditModel class BaseModel(AuditModel): id = models.UUIDField( - default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True + default=uuid.uuid4, + unique=True, + editable=False, + db_index=True, + primary_key=True, ) class Meta: diff --git a/apiserver/plane/db/models/cycle.py b/apiserver/plane/db/models/cycle.py index a441057e1..5251c68ec 100644 --- a/apiserver/plane/db/models/cycle.py +++ b/apiserver/plane/db/models/cycle.py @@ -19,6 +19,7 @@ def get_default_filters(): "subscriber": None, } + def get_default_display_filters(): return { "group_by": None, @@ -30,6 +31,7 @@ def get_default_display_filters(): "calendar_date_range": "", } + def get_default_display_properties(): return { "assignee": True, @@ -47,10 +49,15 @@ def get_default_display_properties(): "updated_on": True, } + class Cycle(ProjectBaseModel): name = models.CharField(max_length=255, verbose_name="Cycle Name") - description = models.TextField(verbose_name="Cycle Description", blank=True) - start_date = models.DateField(verbose_name="Start Date", blank=True, null=True) + description = models.TextField( + verbose_name="Cycle Description", blank=True + ) + start_date = models.DateField( + verbose_name="Start Date", blank=True, null=True + ) end_date = models.DateField(verbose_name="End Date", blank=True, null=True) owned_by = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -134,7 +141,9 @@ class CycleFavorite(ProjectBaseModel): class CycleUserProperties(ProjectBaseModel): cycle = models.ForeignKey( - "db.Cycle", on_delete=models.CASCADE, related_name="cycle_user_properties" + "db.Cycle", + on_delete=models.CASCADE, + related_name="cycle_user_properties", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -143,8 +152,9 @@ class CycleUserProperties(ProjectBaseModel): ) filters = models.JSONField(default=get_default_filters) display_filters = models.JSONField(default=get_default_display_filters) - display_properties = models.JSONField(default=get_default_display_properties) - + display_properties = models.JSONField( + default=get_default_display_properties + ) class Meta: unique_together = ["cycle", "user"] @@ -154,4 +164,4 @@ class CycleUserProperties(ProjectBaseModel): ordering = ("-created_at",) def __str__(self): - return f"{self.cycle.name} {self.user.email}" \ No newline at end of file + return f"{self.cycle.name} {self.user.email}" diff --git a/apiserver/plane/db/models/dashboard.py b/apiserver/plane/db/models/dashboard.py new file mode 100644 index 000000000..05c5a893f --- /dev/null +++ b/apiserver/plane/db/models/dashboard.py @@ -0,0 +1,89 @@ +import uuid + +# Django imports +from django.db import models +from django.conf import settings + +# Module imports +from . import BaseModel +from ..mixins import TimeAuditModel + +class Dashboard(BaseModel): + DASHBOARD_CHOICES = ( + ("workspace", "Workspace"), + ("project", "Project"), + ("home", "Home"), + ("team", "Team"), + ("user", "User"), + ) + name = models.CharField(max_length=255) + description_html = models.TextField(blank=True, default="

") + identifier = models.UUIDField(null=True) + owned_by = models.ForeignKey( + "db.User", + on_delete=models.CASCADE, + related_name="dashboards", + ) + is_default = models.BooleanField(default=False) + type_identifier = models.CharField( + max_length=30, + choices=DASHBOARD_CHOICES, + verbose_name="Dashboard Type", + default="home", + ) + + def __str__(self): + """Return name of the dashboard""" + return f"{self.name}" + + class Meta: + verbose_name = "Dashboard" + verbose_name_plural = "Dashboards" + db_table = "dashboards" + ordering = ("-created_at",) + + +class Widget(TimeAuditModel): + id = models.UUIDField( + default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True + ) + key = models.CharField(max_length=255) + filters = models.JSONField(default=dict) + + def __str__(self): + """Return name of the widget""" + return f"{self.key}" + + class Meta: + verbose_name = "Widget" + verbose_name_plural = "Widgets" + db_table = "widgets" + ordering = ("-created_at",) + + +class DashboardWidget(BaseModel): + widget = models.ForeignKey( + Widget, + on_delete=models.CASCADE, + related_name="dashboard_widgets", + ) + dashboard = models.ForeignKey( + Dashboard, + on_delete=models.CASCADE, + related_name="dashboard_widgets", + ) + is_visible = models.BooleanField(default=True) + sort_order = models.FloatField(default=65535) + filters = models.JSONField(default=dict) + properties = models.JSONField(default=dict) + + def __str__(self): + """Return name of the dashboard""" + return f"{self.dashboard.name} {self.widget.key}" + + class Meta: + unique_together = ("widget", "dashboard") + verbose_name = "Dashboard Widget" + verbose_name_plural = "Dashboard Widgets" + db_table = "dashboard_widgets" + ordering = ("-created_at",) diff --git a/apiserver/plane/db/models/estimate.py b/apiserver/plane/db/models/estimate.py index d95a86316..bb57e788c 100644 --- a/apiserver/plane/db/models/estimate.py +++ b/apiserver/plane/db/models/estimate.py @@ -8,7 +8,9 @@ from . import ProjectBaseModel class Estimate(ProjectBaseModel): name = models.CharField(max_length=255) - description = models.TextField(verbose_name="Estimate Description", blank=True) + description = models.TextField( + verbose_name="Estimate Description", blank=True + ) def __str__(self): """Return name of the estimate""" diff --git a/apiserver/plane/db/models/exporter.py b/apiserver/plane/db/models/exporter.py index 0383807b7..d427eb0f6 100644 --- a/apiserver/plane/db/models/exporter.py +++ b/apiserver/plane/db/models/exporter.py @@ -11,14 +11,20 @@ from django.contrib.postgres.fields import ArrayField # Module imports from . import BaseModel + def generate_token(): return uuid4().hex + class ExporterHistory(BaseModel): workspace = models.ForeignKey( - "db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_exporters" + "db.WorkSpace", + on_delete=models.CASCADE, + related_name="workspace_exporters", + ) + project = ArrayField( + models.UUIDField(default=uuid.uuid4), blank=True, null=True ) - project = ArrayField(models.UUIDField(default=uuid.uuid4), blank=True, null=True) provider = models.CharField( max_length=50, choices=( @@ -40,9 +46,13 @@ class ExporterHistory(BaseModel): reason = models.TextField(blank=True) key = models.TextField(blank=True) url = models.URLField(max_length=800, blank=True, null=True) - token = models.CharField(max_length=255, default=generate_token, unique=True) + token = models.CharField( + max_length=255, default=generate_token, unique=True + ) initiated_by = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="workspace_exporters" + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="workspace_exporters", ) class Meta: diff --git a/apiserver/plane/db/models/importer.py b/apiserver/plane/db/models/importer.py index a2d1d3166..651927458 100644 --- a/apiserver/plane/db/models/importer.py +++ b/apiserver/plane/db/models/importer.py @@ -25,7 +25,9 @@ class Importer(ProjectBaseModel): default="queued", ) initiated_by = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="imports" + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="imports", ) metadata = models.JSONField(default=dict) config = models.JSONField(default=dict) diff --git a/apiserver/plane/db/models/inbox.py b/apiserver/plane/db/models/inbox.py index 6ad88e681..809a11821 100644 --- a/apiserver/plane/db/models/inbox.py +++ b/apiserver/plane/db/models/inbox.py @@ -7,7 +7,9 @@ from plane.db.models import ProjectBaseModel class Inbox(ProjectBaseModel): name = models.CharField(max_length=255) - description = models.TextField(verbose_name="Inbox Description", blank=True) + description = models.TextField( + verbose_name="Inbox Description", blank=True + ) is_default = models.BooleanField(default=False) view_props = models.JSONField(default=dict) @@ -31,12 +33,21 @@ class InboxIssue(ProjectBaseModel): "db.Issue", related_name="issue_inbox", on_delete=models.CASCADE ) status = models.IntegerField( - choices=((-2, "Pending"), (-1, "Rejected"), (0, "Snoozed"), (1, "Accepted"), (2, "Duplicate")), + choices=( + (-2, "Pending"), + (-1, "Rejected"), + (0, "Snoozed"), + (1, "Accepted"), + (2, "Duplicate"), + ), default=-2, ) snoozed_till = models.DateTimeField(null=True) duplicate_to = models.ForeignKey( - "db.Issue", related_name="inbox_duplicate", on_delete=models.SET_NULL, null=True + "db.Issue", + related_name="inbox_duplicate", + on_delete=models.SET_NULL, + null=True, ) source = models.TextField(blank=True, null=True) external_source = models.CharField(max_length=255, null=True, blank=True) diff --git a/apiserver/plane/db/models/integration/__init__.py b/apiserver/plane/db/models/integration/__init__.py index 3bef68708..34b40e57d 100644 --- a/apiserver/plane/db/models/integration/__init__.py +++ b/apiserver/plane/db/models/integration/__init__.py @@ -1,3 +1,8 @@ from .base import Integration, WorkspaceIntegration -from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync +from .github import ( + GithubRepository, + GithubRepositorySync, + GithubIssueSync, + GithubCommentSync, +) from .slack import SlackProjectSync diff --git a/apiserver/plane/db/models/integration/base.py b/apiserver/plane/db/models/integration/base.py index 47db0483c..0c68adfd2 100644 --- a/apiserver/plane/db/models/integration/base.py +++ b/apiserver/plane/db/models/integration/base.py @@ -11,7 +11,11 @@ from plane.db.mixins import AuditModel class Integration(AuditModel): id = models.UUIDField( - default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True + default=uuid.uuid4, + unique=True, + editable=False, + db_index=True, + primary_key=True, ) title = models.CharField(max_length=400) provider = models.CharField(max_length=400, unique=True) @@ -40,14 +44,18 @@ class Integration(AuditModel): class WorkspaceIntegration(BaseModel): workspace = models.ForeignKey( - "db.Workspace", related_name="workspace_integrations", on_delete=models.CASCADE + "db.Workspace", + related_name="workspace_integrations", + on_delete=models.CASCADE, ) # Bot user actor = models.ForeignKey( "db.User", related_name="integrations", on_delete=models.CASCADE ) integration = models.ForeignKey( - "db.Integration", related_name="integrated_workspaces", on_delete=models.CASCADE + "db.Integration", + related_name="integrated_workspaces", + on_delete=models.CASCADE, ) api_token = models.ForeignKey( "db.APIToken", related_name="integrations", on_delete=models.CASCADE diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index f4d152bb1..f3331c874 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -36,10 +36,15 @@ class GithubRepositorySync(ProjectBaseModel): "db.User", related_name="user_syncs", on_delete=models.CASCADE ) workspace_integration = models.ForeignKey( - "db.WorkspaceIntegration", related_name="github_syncs", on_delete=models.CASCADE + "db.WorkspaceIntegration", + related_name="github_syncs", + on_delete=models.CASCADE, ) label = models.ForeignKey( - "db.Label", on_delete=models.SET_NULL, null=True, related_name="repo_syncs" + "db.Label", + on_delete=models.SET_NULL, + null=True, + related_name="repo_syncs", ) def __str__(self): @@ -62,7 +67,9 @@ class GithubIssueSync(ProjectBaseModel): "db.Issue", related_name="github_syncs", on_delete=models.CASCADE ) repository_sync = models.ForeignKey( - "db.GithubRepositorySync", related_name="issue_syncs", on_delete=models.CASCADE + "db.GithubRepositorySync", + related_name="issue_syncs", + on_delete=models.CASCADE, ) def __str__(self): @@ -80,10 +87,14 @@ class GithubIssueSync(ProjectBaseModel): class GithubCommentSync(ProjectBaseModel): repo_comment_id = models.BigIntegerField() comment = models.ForeignKey( - "db.IssueComment", related_name="comment_syncs", on_delete=models.CASCADE + "db.IssueComment", + related_name="comment_syncs", + on_delete=models.CASCADE, ) issue_sync = models.ForeignKey( - "db.GithubIssueSync", related_name="comment_syncs", on_delete=models.CASCADE + "db.GithubIssueSync", + related_name="comment_syncs", + on_delete=models.CASCADE, ) def __str__(self): diff --git a/apiserver/plane/db/models/integration/slack.py b/apiserver/plane/db/models/integration/slack.py index 6b29968f6..72df4dfd7 100644 --- a/apiserver/plane/db/models/integration/slack.py +++ b/apiserver/plane/db/models/integration/slack.py @@ -17,7 +17,9 @@ class SlackProjectSync(ProjectBaseModel): team_id = models.CharField(max_length=30) team_name = models.CharField(max_length=300) workspace_integration = models.ForeignKey( - "db.WorkspaceIntegration", related_name="slack_syncs", on_delete=models.CASCADE + "db.WorkspaceIntegration", + related_name="slack_syncs", + on_delete=models.CASCADE, ) def __str__(self): diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index b14376bc5..43274ea13 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -46,6 +46,7 @@ def get_default_filters(): "subscriber": None, } + def get_default_display_filters(): return { "group_by": None, @@ -57,6 +58,7 @@ def get_default_display_filters(): "calendar_date_range": "", } + def get_default_display_properties(): return { "assignee": True, @@ -115,7 +117,9 @@ class Issue(ProjectBaseModel): related_name="state_issue", ) estimate_point = models.IntegerField( - validators=[MinValueValidator(0), MaxValueValidator(7)], null=True, blank=True + validators=[MinValueValidator(0), MaxValueValidator(7)], + null=True, + blank=True, ) name = models.CharField(max_length=255, verbose_name="Issue Name") description = models.JSONField(blank=True, default=dict) @@ -136,7 +140,9 @@ class Issue(ProjectBaseModel): through="IssueAssignee", through_fields=("issue", "assignee"), ) - sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID") + sequence_id = models.IntegerField( + default=1, verbose_name="Issue Sequence ID" + ) labels = models.ManyToManyField( "db.Label", blank=True, related_name="labels", through="IssueLabel" ) @@ -163,7 +169,9 @@ class Issue(ProjectBaseModel): from plane.db.models import State default_state = State.objects.filter( - ~models.Q(name="Triage"), project=self.project, default=True + ~models.Q(name="Triage"), + project=self.project, + default=True, ).first() # if there is no default state assign any random state if default_state is None: @@ -176,12 +184,11 @@ class Issue(ProjectBaseModel): except ImportError: pass - if self._state.adding: # Get the maximum display_id value from the database - last_id = IssueSequence.objects.filter(project=self.project).aggregate( - largest=models.Max("sequence") - )["largest"] + last_id = IssueSequence.objects.filter( + project=self.project + ).aggregate(largest=models.Max("sequence"))["largest"] # aggregate can return None! Check it first. # If it isn't none, just use the last ID specified (which should be the greatest) and add one to it if last_id: @@ -254,8 +261,9 @@ class IssueRelation(ProjectBaseModel): ordering = ("-created_at",) def __str__(self): - return f"{self.issue.name} {self.related_issue.name}" - + return f"{self.issue.name} {self.related_issue.name}" + + class IssueMention(ProjectBaseModel): issue = models.ForeignKey( Issue, on_delete=models.CASCADE, related_name="issue_mention" @@ -265,6 +273,7 @@ class IssueMention(ProjectBaseModel): on_delete=models.CASCADE, related_name="issue_mention", ) + class Meta: unique_together = ["issue", "mention"] verbose_name = "Issue Mention" @@ -273,7 +282,7 @@ class IssueMention(ProjectBaseModel): ordering = ("-created_at",) def __str__(self): - return f"{self.issue.name} {self.mention.email}" + return f"{self.issue.name} {self.mention.email}" class IssueAssignee(ProjectBaseModel): @@ -349,17 +358,28 @@ class IssueAttachment(ProjectBaseModel): class IssueActivity(ProjectBaseModel): issue = models.ForeignKey( - Issue, on_delete=models.SET_NULL, null=True, related_name="issue_activity" + Issue, + on_delete=models.SET_NULL, + null=True, + related_name="issue_activity", + ) + verb = models.CharField( + max_length=255, verbose_name="Action", default="created" ) - verb = models.CharField(max_length=255, verbose_name="Action", default="created") field = models.CharField( max_length=255, verbose_name="Field Name", blank=True, null=True ) - old_value = models.TextField(verbose_name="Old Value", blank=True, null=True) - new_value = models.TextField(verbose_name="New Value", blank=True, null=True) + old_value = models.TextField( + verbose_name="Old Value", blank=True, null=True + ) + new_value = models.TextField( + verbose_name="New Value", blank=True, null=True + ) comment = models.TextField(verbose_name="Comment", blank=True) - attachments = ArrayField(models.URLField(), size=10, blank=True, default=list) + attachments = ArrayField( + models.URLField(), size=10, blank=True, default=list + ) issue_comment = models.ForeignKey( "db.IssueComment", on_delete=models.SET_NULL, @@ -391,7 +411,9 @@ class IssueComment(ProjectBaseModel): comment_stripped = models.TextField(verbose_name="Comment", blank=True) comment_json = models.JSONField(blank=True, default=dict) comment_html = models.TextField(blank=True, default="

") - attachments = ArrayField(models.URLField(), size=10, blank=True, default=list) + attachments = ArrayField( + models.URLField(), size=10, blank=True, default=list + ) issue = models.ForeignKey( Issue, on_delete=models.CASCADE, related_name="issue_comments" ) @@ -438,7 +460,9 @@ class IssueProperty(ProjectBaseModel): ) filters = models.JSONField(default=get_default_filters) display_filters = models.JSONField(default=get_default_display_filters) - display_properties = models.JSONField(default=get_default_display_properties) + display_properties = models.JSONField( + default=get_default_display_properties + ) class Meta: verbose_name = "Issue Property" @@ -510,7 +534,10 @@ class IssueLabel(ProjectBaseModel): class IssueSequence(ProjectBaseModel): issue = models.ForeignKey( - Issue, on_delete=models.SET_NULL, related_name="issue_sequence", null=True + Issue, + on_delete=models.SET_NULL, + related_name="issue_sequence", + null=True, ) sequence = models.PositiveBigIntegerField(default=1) deleted = models.BooleanField(default=False) @@ -572,7 +599,9 @@ class CommentReaction(ProjectBaseModel): related_name="comment_reactions", ) comment = models.ForeignKey( - IssueComment, on_delete=models.CASCADE, related_name="comment_reactions" + IssueComment, + on_delete=models.CASCADE, + related_name="comment_reactions", ) reaction = models.CharField(max_length=20) @@ -588,9 +617,13 @@ class CommentReaction(ProjectBaseModel): class IssueVote(ProjectBaseModel): - issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="votes") + issue = models.ForeignKey( + Issue, on_delete=models.CASCADE, related_name="votes" + ) actor = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="votes" + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="votes", ) vote = models.IntegerField( choices=( @@ -619,5 +652,7 @@ class IssueVote(ProjectBaseModel): def create_issue_sequence(sender, instance, created, **kwargs): if created: IssueSequence.objects.create( - issue=instance, sequence=instance.sequence_id, project=instance.project + issue=instance, + sequence=instance.sequence_id, + project=instance.project, ) diff --git a/apiserver/plane/db/models/module.py b/apiserver/plane/db/models/module.py index cc8185946..1a47aac1b 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -7,17 +7,20 @@ from . import ProjectBaseModel def get_default_filters(): - return { - "priority": None, - "state": None, - "state_group": None, - "assignees": None, - "created_by": None, - "labels": None, - "start_date": None, - "target_date": None, - "subscriber": None, - }, + return ( + { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + }, + ) + def get_default_display_filters(): return { @@ -30,6 +33,7 @@ def get_default_display_filters(): "calendar_date_range": "", } + def get_default_display_properties(): return { "assignee": True, @@ -47,9 +51,12 @@ def get_default_display_properties(): "updated_on": True, } + class Module(ProjectBaseModel): name = models.CharField(max_length=255, verbose_name="Module Name") - description = models.TextField(verbose_name="Module Description", blank=True) + description = models.TextField( + verbose_name="Module Description", blank=True + ) description_text = models.JSONField( verbose_name="Module Description RT", blank=True, null=True ) @@ -71,7 +78,10 @@ class Module(ProjectBaseModel): max_length=20, ) lead = models.ForeignKey( - "db.User", on_delete=models.SET_NULL, related_name="module_leads", null=True + "db.User", + on_delete=models.SET_NULL, + related_name="module_leads", + null=True, ) members = models.ManyToManyField( settings.AUTH_USER_MODEL, @@ -94,9 +104,9 @@ class Module(ProjectBaseModel): def save(self, *args, **kwargs): if self._state.adding: - smallest_sort_order = Module.objects.filter(project=self.project).aggregate( - smallest=models.Min("sort_order") - )["smallest"] + smallest_sort_order = Module.objects.filter( + project=self.project + ).aggregate(smallest=models.Min("sort_order"))["smallest"] if smallest_sort_order is not None: self.sort_order = smallest_sort_order - 10000 @@ -186,7 +196,9 @@ class ModuleFavorite(ProjectBaseModel): class ModuleUserProperties(ProjectBaseModel): module = models.ForeignKey( - "db.Module", on_delete=models.CASCADE, related_name="module_user_properties" + "db.Module", + on_delete=models.CASCADE, + related_name="module_user_properties", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -195,8 +207,9 @@ class ModuleUserProperties(ProjectBaseModel): ) filters = models.JSONField(default=get_default_filters) display_filters = models.JSONField(default=get_default_display_filters) - display_properties = models.JSONField(default=get_default_display_properties) - + display_properties = models.JSONField( + default=get_default_display_properties + ) class Meta: unique_together = ["module", "user"] @@ -206,4 +219,4 @@ class ModuleUserProperties(ProjectBaseModel): ordering = ("-created_at",) def __str__(self): - return f"{self.module.name} {self.user.email}" \ No newline at end of file + return f"{self.module.name} {self.user.email}" diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py index 3df935718..8e6a48e14 100644 --- a/apiserver/plane/db/models/notification.py +++ b/apiserver/plane/db/models/notification.py @@ -10,7 +10,10 @@ class Notification(BaseModel): "db.Workspace", related_name="notifications", on_delete=models.CASCADE ) project = models.ForeignKey( - "db.Project", related_name="notifications", on_delete=models.CASCADE, null=True + "db.Project", + related_name="notifications", + on_delete=models.CASCADE, + null=True, ) data = models.JSONField(null=True) entity_identifier = models.UUIDField(null=True) @@ -20,8 +23,17 @@ class Notification(BaseModel): message_html = models.TextField(blank=True, default="

") message_stripped = models.TextField(blank=True, null=True) sender = models.CharField(max_length=255) - triggered_by = models.ForeignKey("db.User", related_name="triggered_notifications", on_delete=models.SET_NULL, null=True) - receiver = models.ForeignKey("db.User", related_name="received_notifications", on_delete=models.CASCADE) + triggered_by = models.ForeignKey( + "db.User", + related_name="triggered_notifications", + on_delete=models.SET_NULL, + null=True, + ) + receiver = models.ForeignKey( + "db.User", + related_name="received_notifications", + on_delete=models.CASCADE, + ) read_at = models.DateTimeField(null=True) snoozed_till = models.DateTimeField(null=True) archived_at = models.DateTimeField(null=True) diff --git a/apiserver/plane/db/models/page.py b/apiserver/plane/db/models/page.py index de65cb98f..6ed94798a 100644 --- a/apiserver/plane/db/models/page.py +++ b/apiserver/plane/db/models/page.py @@ -15,7 +15,9 @@ class Page(ProjectBaseModel): description_html = models.TextField(blank=True, default="

") description_stripped = models.TextField(blank=True, null=True) owned_by = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="pages" + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="pages", ) access = models.PositiveSmallIntegerField( choices=((0, "Public"), (1, "Private")), default=0 @@ -53,7 +55,7 @@ class PageLog(ProjectBaseModel): ("video", "Video"), ("file", "File"), ("link", "Link"), - ("cycle","Cycle"), + ("cycle", "Cycle"), ("module", "Module"), ("back_link", "Back Link"), ("forward_link", "Forward Link"), @@ -77,13 +79,15 @@ class PageLog(ProjectBaseModel): verbose_name_plural = "Page Logs" db_table = "page_logs" ordering = ("-created_at",) - + def __str__(self): return f"{self.page.name} {self.type}" class PageBlock(ProjectBaseModel): - page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="blocks") + page = models.ForeignKey( + "db.Page", on_delete=models.CASCADE, related_name="blocks" + ) name = models.CharField(max_length=255) description = models.JSONField(default=dict, blank=True) description_html = models.TextField(blank=True, default="

") @@ -118,7 +122,9 @@ class PageBlock(ProjectBaseModel): group="completed", project=self.project ).first() if completed_state is not None: - Issue.objects.update(pk=self.issue_id, state=completed_state) + Issue.objects.update( + pk=self.issue_id, state=completed_state + ) except ImportError: pass super(PageBlock, self).save(*args, **kwargs) diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index fe72c260b..b93174724 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -35,7 +35,7 @@ def get_default_props(): }, "display_filters": { "group_by": None, - "order_by": '-created_at', + "order_by": "-created_at", "type": None, "sub_issue": True, "show_empty_groups": True, @@ -52,16 +52,22 @@ def get_default_preferences(): class Project(BaseModel): NETWORK_CHOICES = ((0, "Secret"), (2, "Public")) name = models.CharField(max_length=255, verbose_name="Project Name") - description = models.TextField(verbose_name="Project Description", blank=True) + description = models.TextField( + verbose_name="Project Description", blank=True + ) description_text = models.JSONField( verbose_name="Project Description RT", blank=True, null=True ) description_html = models.JSONField( verbose_name="Project Description HTML", blank=True, null=True ) - network = models.PositiveSmallIntegerField(default=2, choices=NETWORK_CHOICES) + network = models.PositiveSmallIntegerField( + default=2, choices=NETWORK_CHOICES + ) workspace = models.ForeignKey( - "db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_project" + "db.WorkSpace", + on_delete=models.CASCADE, + related_name="workspace_project", ) identifier = models.CharField( max_length=12, @@ -90,7 +96,10 @@ class Project(BaseModel): inbox_view = models.BooleanField(default=False) cover_image = models.URLField(blank=True, null=True, max_length=800) estimate = models.ForeignKey( - "db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True + "db.Estimate", + on_delete=models.SET_NULL, + related_name="projects", + null=True, ) archive_in = models.IntegerField( default=0, validators=[MinValueValidator(0), MaxValueValidator(12)] @@ -99,7 +108,10 @@ class Project(BaseModel): default=0, validators=[MinValueValidator(0), MaxValueValidator(12)] ) default_state = models.ForeignKey( - "db.State", on_delete=models.SET_NULL, null=True, related_name="default_state" + "db.State", + on_delete=models.SET_NULL, + null=True, + related_name="default_state", ) def __str__(self): @@ -195,7 +207,10 @@ class ProjectMember(ProjectBaseModel): # TODO: Remove workspace relation later class ProjectIdentifier(AuditModel): workspace = models.ForeignKey( - "db.Workspace", models.CASCADE, related_name="project_identifiers", null=True + "db.Workspace", + models.CASCADE, + related_name="project_identifiers", + null=True, ) project = models.OneToOneField( Project, on_delete=models.CASCADE, related_name="project_identifier" @@ -250,7 +265,10 @@ class ProjectDeployBoard(ProjectBaseModel): comments = models.BooleanField(default=False) reactions = models.BooleanField(default=False) inbox = models.ForeignKey( - "db.Inbox", related_name="bord_inbox", on_delete=models.SET_NULL, null=True + "db.Inbox", + related_name="bord_inbox", + on_delete=models.SET_NULL, + null=True, ) votes = models.BooleanField(default=False) views = models.JSONField(default=get_default_views) diff --git a/apiserver/plane/db/models/state.py b/apiserver/plane/db/models/state.py index 3370f239d..ab9b780c8 100644 --- a/apiserver/plane/db/models/state.py +++ b/apiserver/plane/db/models/state.py @@ -8,7 +8,9 @@ from . import ProjectBaseModel class State(ProjectBaseModel): name = models.CharField(max_length=255, verbose_name="State Name") - description = models.TextField(verbose_name="State Description", blank=True) + description = models.TextField( + verbose_name="State Description", blank=True + ) color = models.CharField(max_length=255, verbose_name="State Color") slug = models.SlugField(max_length=100, blank=True) sequence = models.FloatField(default=65535) diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index fe75a6a26..087162ca5 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -6,16 +6,12 @@ import pytz # Django imports from django.db import models -from django.db.models.signals import post_save -from django.dispatch import receiver -from django.contrib.auth.models import AbstractBaseUser, UserManager, PermissionsMixin +from django.contrib.auth.models import ( + AbstractBaseUser, + UserManager, + PermissionsMixin, +) from django.utils import timezone -from django.conf import settings - -# Third party imports -from sentry_sdk import capture_exception -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError def get_default_onboarding(): @@ -29,22 +25,34 @@ def get_default_onboarding(): class User(AbstractBaseUser, PermissionsMixin): id = models.UUIDField( - default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True + default=uuid.uuid4, + unique=True, + editable=False, + db_index=True, + primary_key=True, ) username = models.CharField(max_length=128, unique=True) # user fields mobile_number = models.CharField(max_length=255, blank=True, null=True) - email = models.CharField(max_length=255, null=True, blank=True, unique=True) + email = models.CharField( + max_length=255, null=True, blank=True, unique=True + ) first_name = models.CharField(max_length=255, blank=True) last_name = models.CharField(max_length=255, blank=True) avatar = models.CharField(max_length=255, blank=True) cover_image = models.URLField(blank=True, null=True, max_length=800) # tracking metrics - date_joined = models.DateTimeField(auto_now_add=True, verbose_name="Created At") - created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created At") - updated_at = models.DateTimeField(auto_now=True, verbose_name="Last Modified At") + date_joined = models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ) + created_at = models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ) + updated_at = models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ) last_location = models.CharField(max_length=255, blank=True) created_location = models.CharField(max_length=255, blank=True) @@ -65,7 +73,9 @@ class User(AbstractBaseUser, PermissionsMixin): has_billing_address = models.BooleanField(default=False) USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) - user_timezone = models.CharField(max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES) + user_timezone = models.CharField( + max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES + ) last_active = models.DateTimeField(default=timezone.now, null=True) last_login_time = models.DateTimeField(null=True) @@ -115,30 +125,12 @@ class User(AbstractBaseUser, PermissionsMixin): self.display_name = ( self.email.split("@")[0] if len(self.email.split("@")) - else "".join(random.choice(string.ascii_letters) for _ in range(6)) + else "".join( + random.choice(string.ascii_letters) for _ in range(6) + ) ) if self.is_superuser: self.is_staff = True super(User, self).save(*args, **kwargs) - - -@receiver(post_save, sender=User) -def send_welcome_slack(sender, instance, created, **kwargs): - try: - if created and not instance.is_bot: - # Send message on slack as well - if settings.SLACK_BOT_TOKEN: - client = WebClient(token=settings.SLACK_BOT_TOKEN) - try: - _ = client.chat_postMessage( - channel="#trackers", - text=f"New user {instance.email} has signed up and begun the onboarding journey.", - ) - except SlackApiError as e: - print(f"Got an error: {e.response['error']}") - return - except Exception as e: - capture_exception(e) - return diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index 8a77f0586..13500b5a4 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -19,6 +19,7 @@ def get_default_filters(): "subscriber": None, } + def get_default_display_filters(): return { "group_by": None, @@ -30,6 +31,7 @@ def get_default_display_filters(): "calendar_date_range": "", } + def get_default_display_properties(): return { "assignee": True, @@ -47,6 +49,7 @@ def get_default_display_properties(): "updated_on": True, } + class GlobalView(BaseModel): workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="global_views" @@ -65,7 +68,7 @@ class GlobalView(BaseModel): verbose_name_plural = "Global Views" db_table = "global_views" ordering = ("-created_at",) - + def save(self, *args, **kwargs): if self._state.adding: largest_sort_order = GlobalView.objects.filter( @@ -87,7 +90,9 @@ class IssueView(WorkspaceBaseModel): query = models.JSONField(verbose_name="View Query") filters = models.JSONField(default=dict) display_filters = models.JSONField(default=get_default_display_filters) - display_properties = models.JSONField(default=get_default_display_properties) + display_properties = models.JSONField( + default=get_default_display_properties + ) access = models.PositiveSmallIntegerField( default=1, choices=((0, "Private"), (1, "Public")) ) diff --git a/apiserver/plane/db/models/webhook.py b/apiserver/plane/db/models/webhook.py index ea2b508e5..fbe74d03a 100644 --- a/apiserver/plane/db/models/webhook.py +++ b/apiserver/plane/db/models/webhook.py @@ -17,7 +17,9 @@ def generate_token(): def validate_schema(value): parsed_url = urlparse(value) if parsed_url.scheme not in ["http", "https"]: - raise ValidationError("Invalid schema. Only HTTP and HTTPS are allowed.") + raise ValidationError( + "Invalid schema. Only HTTP and HTTPS are allowed." + ) def validate_domain(value): @@ -63,7 +65,9 @@ class WebhookLog(BaseModel): "db.Workspace", on_delete=models.CASCADE, related_name="webhook_logs" ) # Associated webhook - webhook = models.ForeignKey(Webhook, on_delete=models.CASCADE, related_name="logs") + webhook = models.ForeignKey( + Webhook, on_delete=models.CASCADE, related_name="logs" + ) # Basic request details event_type = models.CharField(max_length=255, blank=True, null=True) diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index f0d64ecae..7e5d6d90b 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -54,6 +54,7 @@ def get_default_props(): }, } + def get_default_filters(): return { "priority": None, @@ -67,6 +68,7 @@ def get_default_filters(): "subscriber": None, } + def get_default_display_filters(): return { "display_filters": { @@ -80,6 +82,7 @@ def get_default_display_filters(): }, } + def get_default_display_properties(): return { "display_properties": { @@ -134,7 +137,14 @@ class Workspace(BaseModel): on_delete=models.CASCADE, related_name="owner_workspace", ) - slug = models.SlugField(max_length=48, db_index=True, unique=True, validators=[slug_validator,]) + slug = models.SlugField( + max_length=48, + db_index=True, + unique=True, + validators=[ + slug_validator, + ], + ) organization_size = models.CharField(max_length=20, blank=True, null=True) def __str__(self): @@ -153,20 +163,26 @@ class WorkspaceBaseModel(BaseModel): "db.Workspace", models.CASCADE, related_name="workspace_%(class)s" ) project = models.ForeignKey( - "db.Project", models.CASCADE, related_name="project_%(class)s", null=True + "db.Project", + models.CASCADE, + related_name="project_%(class)s", + null=True, ) class Meta: abstract = True - + def save(self, *args, **kwargs): if self.project: self.workspace = self.project.workspace super(WorkspaceBaseModel, self).save(*args, **kwargs) + class WorkspaceMember(BaseModel): workspace = models.ForeignKey( - "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member" + "db.Workspace", + on_delete=models.CASCADE, + related_name="workspace_member", ) member = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -194,7 +210,9 @@ class WorkspaceMember(BaseModel): class WorkspaceMemberInvite(BaseModel): workspace = models.ForeignKey( - "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member_invite" + "db.Workspace", + on_delete=models.CASCADE, + related_name="workspace_member_invite", ) email = models.CharField(max_length=255) accepted = models.BooleanField(default=False) @@ -244,9 +262,13 @@ class TeamMember(BaseModel): workspace = models.ForeignKey( Workspace, on_delete=models.CASCADE, related_name="team_member" ) - team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="team_member") + team = models.ForeignKey( + Team, on_delete=models.CASCADE, related_name="team_member" + ) member = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="team_member" + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="team_member", ) def __str__(self): @@ -266,7 +288,9 @@ class WorkspaceTheme(BaseModel): ) name = models.CharField(max_length=300) actor = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="themes" + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="themes", ) colors = models.JSONField(default=dict) @@ -283,7 +307,9 @@ class WorkspaceTheme(BaseModel): class WorkspaceUserProperties(BaseModel): workspace = models.ForeignKey( - "db.Workspace", on_delete=models.CASCADE, related_name="workspace_user_properties" + "db.Workspace", + on_delete=models.CASCADE, + related_name="workspace_user_properties", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -292,8 +318,9 @@ class WorkspaceUserProperties(BaseModel): ) filters = models.JSONField(default=get_default_filters) display_filters = models.JSONField(default=get_default_display_filters) - display_properties = models.JSONField(default=get_default_display_properties) - + display_properties = models.JSONField( + default=get_default_display_properties + ) class Meta: unique_together = ["workspace", "user"] @@ -303,4 +330,4 @@ class WorkspaceUserProperties(BaseModel): ordering = ("-created_at",) def __str__(self): - return f"{self.workspace.name} {self.user.email}" \ No newline at end of file + return f"{self.workspace.name} {self.user.email}" diff --git a/apiserver/plane/license/api/permissions/instance.py b/apiserver/plane/license/api/permissions/instance.py index dff16605a..9ee85404b 100644 --- a/apiserver/plane/license/api/permissions/instance.py +++ b/apiserver/plane/license/api/permissions/instance.py @@ -7,7 +7,6 @@ from plane.license.models import Instance, InstanceAdmin class InstanceAdminPermission(BasePermission): def has_permission(self, request, view): - if request.user.is_anonymous: return False diff --git a/apiserver/plane/license/api/serializers/__init__.py b/apiserver/plane/license/api/serializers/__init__.py index b658ff148..e6beda0e9 100644 --- a/apiserver/plane/license/api/serializers/__init__.py +++ b/apiserver/plane/license/api/serializers/__init__.py @@ -1 +1,5 @@ -from .instance import InstanceSerializer, InstanceAdminSerializer, InstanceConfigurationSerializer \ No newline at end of file +from .instance import ( + InstanceSerializer, + InstanceAdminSerializer, + InstanceConfigurationSerializer, +) diff --git a/apiserver/plane/license/api/serializers/instance.py b/apiserver/plane/license/api/serializers/instance.py index 173d718d9..8a99acbae 100644 --- a/apiserver/plane/license/api/serializers/instance.py +++ b/apiserver/plane/license/api/serializers/instance.py @@ -4,8 +4,11 @@ from plane.app.serializers import BaseSerializer from plane.app.serializers import UserAdminLiteSerializer from plane.license.utils.encryption import decrypt_data + class InstanceSerializer(BaseSerializer): - primary_owner_details = UserAdminLiteSerializer(source="primary_owner", read_only=True) + primary_owner_details = UserAdminLiteSerializer( + source="primary_owner", read_only=True + ) class Meta: model = Instance @@ -34,8 +37,8 @@ class InstanceAdminSerializer(BaseSerializer): "user", ] -class InstanceConfigurationSerializer(BaseSerializer): +class InstanceConfigurationSerializer(BaseSerializer): class Meta: model = InstanceConfiguration fields = "__all__" diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index c88b3b75f..112c68bc8 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -61,7 +61,9 @@ class InstanceEndpoint(BaseAPIView): def patch(self, request): # Get the instance instance = Instance.objects.first() - serializer = InstanceSerializer(instance, data=request.data, partial=True) + serializer = InstanceSerializer( + instance, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) @@ -80,7 +82,8 @@ class InstanceAdminEndpoint(BaseAPIView): if not email: return Response( - {"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Email is required"}, + status=status.HTTP_400_BAD_REQUEST, ) instance = Instance.objects.first() @@ -114,7 +117,9 @@ class InstanceAdminEndpoint(BaseAPIView): def delete(self, request, pk): instance = Instance.objects.first() - instance_admin = InstanceAdmin.objects.filter(instance=instance, pk=pk).delete() + instance_admin = InstanceAdmin.objects.filter( + instance=instance, pk=pk + ).delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -125,7 +130,9 @@ class InstanceConfigurationEndpoint(BaseAPIView): def get(self, request): instance_configurations = InstanceConfiguration.objects.all() - serializer = InstanceConfigurationSerializer(instance_configurations, many=True) + serializer = InstanceConfigurationSerializer( + instance_configurations, many=True + ) return Response(serializer.data, status=status.HTTP_200_OK) def patch(self, request): diff --git a/apiserver/plane/license/management/commands/configure_instance.py b/apiserver/plane/license/management/commands/configure_instance.py index 67137d0d9..f81d98cba 100644 --- a/apiserver/plane/license/management/commands/configure_instance.py +++ b/apiserver/plane/license/management/commands/configure_instance.py @@ -21,7 +21,7 @@ class Command(BaseCommand): "key": "ENABLE_SIGNUP", "value": os.environ.get("ENABLE_SIGNUP", "1"), "category": "AUTHENTICATION", - "is_encrypted": False, + "is_encrypted": False, }, { "key": "ENABLE_EMAIL_PASSWORD", @@ -128,5 +128,7 @@ class Command(BaseCommand): ) else: self.stdout.write( - self.style.WARNING(f"{obj.key} configuration already exists") + self.style.WARNING( + f"{obj.key} configuration already exists" + ) ) diff --git a/apiserver/plane/license/management/commands/register_instance.py b/apiserver/plane/license/management/commands/register_instance.py index e6cfa7167..889cd46dc 100644 --- a/apiserver/plane/license/management/commands/register_instance.py +++ b/apiserver/plane/license/management/commands/register_instance.py @@ -12,13 +12,15 @@ from django.conf import settings from plane.license.models import Instance from plane.db.models import User + class Command(BaseCommand): help = "Check if instance in registered else register" def add_arguments(self, parser): # Positional argument - parser.add_argument('machine_signature', type=str, help='Machine signature') - + parser.add_argument( + "machine_signature", type=str, help="Machine signature" + ) def handle(self, *args, **options): # Check if the instance is registered @@ -30,7 +32,9 @@ class Command(BaseCommand): # Load JSON content from the file data = json.load(file) - machine_signature = options.get("machine_signature", "machine-signature") + machine_signature = options.get( + "machine_signature", "machine-signature" + ) if not machine_signature: raise CommandError("Machine signature is required") @@ -52,15 +56,9 @@ class Command(BaseCommand): user_count=payload.get("user_count", 0), ) - self.stdout.write( - self.style.SUCCESS( - f"Instance registered" - ) - ) + self.stdout.write(self.style.SUCCESS(f"Instance registered")) else: self.stdout.write( - self.style.SUCCESS( - f"Instance already registered" - ) + self.style.SUCCESS(f"Instance already registered") ) return diff --git a/apiserver/plane/license/migrations/0001_initial.py b/apiserver/plane/license/migrations/0001_initial.py index c8b5f1f02..4eed3adf7 100644 --- a/apiserver/plane/license/migrations/0001_initial.py +++ b/apiserver/plane/license/migrations/0001_initial.py @@ -7,7 +7,6 @@ import uuid class Migration(migrations.Migration): - initial = True dependencies = [ @@ -16,74 +15,220 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Instance', + name="Instance", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('instance_name', models.CharField(max_length=255)), - ('whitelist_emails', models.TextField(blank=True, null=True)), - ('instance_id', models.CharField(max_length=25, unique=True)), - ('license_key', models.CharField(blank=True, max_length=256, null=True)), - ('api_key', models.CharField(max_length=16)), - ('version', models.CharField(max_length=10)), - ('last_checked_at', models.DateTimeField()), - ('namespace', models.CharField(blank=True, max_length=50, null=True)), - ('is_telemetry_enabled', models.BooleanField(default=True)), - ('is_support_required', models.BooleanField(default=True)), - ('is_setup_done', models.BooleanField(default=False)), - ('is_signup_screen_visited', models.BooleanField(default=False)), - ('user_count', models.PositiveBigIntegerField(default=0)), - ('is_verified', models.BooleanField(default=False)), - ('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')), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("instance_name", models.CharField(max_length=255)), + ("whitelist_emails", models.TextField(blank=True, null=True)), + ("instance_id", models.CharField(max_length=25, unique=True)), + ( + "license_key", + models.CharField(blank=True, max_length=256, null=True), + ), + ("api_key", models.CharField(max_length=16)), + ("version", models.CharField(max_length=10)), + ("last_checked_at", models.DateTimeField()), + ( + "namespace", + models.CharField(blank=True, max_length=50, null=True), + ), + ("is_telemetry_enabled", models.BooleanField(default=True)), + ("is_support_required", models.BooleanField(default=True)), + ("is_setup_done", models.BooleanField(default=False)), + ( + "is_signup_screen_visited", + models.BooleanField(default=False), + ), + ("user_count", models.PositiveBigIntegerField(default=0)), + ("is_verified", models.BooleanField(default=False)), + ( + "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", + ), + ), + ( + "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': 'Instance', - 'verbose_name_plural': 'Instances', - 'db_table': 'instances', - 'ordering': ('-created_at',), + "verbose_name": "Instance", + "verbose_name_plural": "Instances", + "db_table": "instances", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='InstanceConfiguration', + name="InstanceConfiguration", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('key', models.CharField(max_length=100, unique=True)), - ('value', models.TextField(blank=True, default=None, null=True)), - ('category', models.TextField()), - ('is_encrypted', models.BooleanField(default=False)), - ('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')), - ('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')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("key", models.CharField(max_length=100, unique=True)), + ( + "value", + models.TextField(blank=True, default=None, null=True), + ), + ("category", models.TextField()), + ("is_encrypted", models.BooleanField(default=False)), + ( + "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", + ), + ), + ( + "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': 'Instance Configuration', - 'verbose_name_plural': 'Instance Configurations', - 'db_table': 'instance_configurations', - 'ordering': ('-created_at',), + "verbose_name": "Instance Configuration", + "verbose_name_plural": "Instance Configurations", + "db_table": "instance_configurations", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='InstanceAdmin', + name="InstanceAdmin", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('role', models.PositiveIntegerField(choices=[(20, 'Admin')], default=20)), - ('is_verified', models.BooleanField(default=False)), - ('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')), - ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admins', to='license.instance')), - ('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')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_owner', to=settings.AUTH_USER_MODEL)), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "role", + models.PositiveIntegerField( + choices=[(20, "Admin")], default=20 + ), + ), + ("is_verified", models.BooleanField(default=False)), + ( + "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", + ), + ), + ( + "instance", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="admins", + to="license.instance", + ), + ), + ( + "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", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="instance_owner", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'verbose_name': 'Instance Admin', - 'verbose_name_plural': 'Instance Admins', - 'db_table': 'instance_admins', - 'ordering': ('-created_at',), - 'unique_together': {('instance', 'user')}, + "verbose_name": "Instance Admin", + "verbose_name_plural": "Instance Admins", + "db_table": "instance_admins", + "ordering": ("-created_at",), + "unique_together": {("instance", "user")}, }, ), ] diff --git a/apiserver/plane/license/models/__init__.py b/apiserver/plane/license/models/__init__.py index 28f2c4352..0f35f718d 100644 --- a/apiserver/plane/license/models/__init__.py +++ b/apiserver/plane/license/models/__init__.py @@ -1 +1 @@ -from .instance import Instance, InstanceAdmin, InstanceConfiguration \ No newline at end of file +from .instance import Instance, InstanceAdmin, InstanceConfiguration diff --git a/apiserver/plane/license/models/instance.py b/apiserver/plane/license/models/instance.py index 86845c34b..b8957e44f 100644 --- a/apiserver/plane/license/models/instance.py +++ b/apiserver/plane/license/models/instance.py @@ -5,9 +5,7 @@ from django.conf import settings # Module imports from plane.db.models import BaseModel -ROLE_CHOICES = ( - (20, "Admin"), -) +ROLE_CHOICES = ((20, "Admin"),) class Instance(BaseModel): @@ -46,7 +44,9 @@ class InstanceAdmin(BaseModel): null=True, related_name="instance_owner", ) - instance = models.ForeignKey(Instance, on_delete=models.CASCADE, related_name="admins") + instance = models.ForeignKey( + Instance, on_delete=models.CASCADE, related_name="admins" + ) role = models.PositiveIntegerField(choices=ROLE_CHOICES, default=20) is_verified = models.BooleanField(default=False) @@ -70,4 +70,3 @@ class InstanceConfiguration(BaseModel): verbose_name_plural = "Instance Configurations" db_table = "instance_configurations" ordering = ("-created_at",) - diff --git a/apiserver/plane/license/urls.py b/apiserver/plane/license/urls.py index 807833a7e..e6315e021 100644 --- a/apiserver/plane/license/urls.py +++ b/apiserver/plane/license/urls.py @@ -10,32 +10,32 @@ from plane.license.api.views import ( urlpatterns = [ path( - "instances/", + "", InstanceEndpoint.as_view(), name="instance", ), path( - "instances/admins/", + "admins/", InstanceAdminEndpoint.as_view(), name="instance-admins", ), path( - "instances/admins//", + "admins//", InstanceAdminEndpoint.as_view(), name="instance-admins", ), path( - "instances/configurations/", + "configurations/", InstanceConfigurationEndpoint.as_view(), name="instance-configuration", ), path( - "instances/admins/sign-in/", + "admins/sign-in/", InstanceAdminSignInEndpoint.as_view(), name="instance-admin-sign-in", ), path( - "instances/admins/sign-up-screen-visited/", + "admins/sign-up-screen-visited/", SignUpScreenVisitedEndpoint.as_view(), name="instance-sign-up", ), diff --git a/apiserver/plane/license/utils/encryption.py b/apiserver/plane/license/utils/encryption.py index c2d369c2e..11bd9000e 100644 --- a/apiserver/plane/license/utils/encryption.py +++ b/apiserver/plane/license/utils/encryption.py @@ -6,9 +6,10 @@ from cryptography.fernet import Fernet def derive_key(secret_key): # Use a key derivation function to get a suitable encryption key - dk = hashlib.pbkdf2_hmac('sha256', secret_key.encode(), b'salt', 100000) + dk = hashlib.pbkdf2_hmac("sha256", secret_key.encode(), b"salt", 100000) return base64.urlsafe_b64encode(dk) + # Encrypt data def encrypt_data(data): if data: @@ -18,11 +19,14 @@ def encrypt_data(data): else: return "" -# Decrypt data + +# Decrypt data def decrypt_data(encrypted_data): if encrypted_data: cipher_suite = Fernet(derive_key(settings.SECRET_KEY)) - decrypted_data = cipher_suite.decrypt(encrypted_data.encode()) # Convert string back to bytes + decrypted_data = cipher_suite.decrypt( + encrypted_data.encode() + ) # Convert string back to bytes return decrypted_data.decode() else: - return "" \ No newline at end of file + return "" diff --git a/apiserver/plane/license/utils/instance_value.py b/apiserver/plane/license/utils/instance_value.py index e56525893..bc4fd5d21 100644 --- a/apiserver/plane/license/utils/instance_value.py +++ b/apiserver/plane/license/utils/instance_value.py @@ -22,7 +22,9 @@ def get_configuration_value(keys): for item in instance_configuration: if key.get("key") == item.get("key"): if item.get("is_encrypted", False): - environment_list.append(decrypt_data(item.get("value"))) + environment_list.append( + decrypt_data(item.get("value")) + ) else: environment_list.append(item.get("value")) @@ -32,40 +34,41 @@ def get_configuration_value(keys): else: # Get the configuration from os for key in keys: - environment_list.append(os.environ.get(key.get("key"), key.get("default"))) + environment_list.append( + os.environ.get(key.get("key"), key.get("default")) + ) return tuple(environment_list) def get_email_configuration(): - return ( - get_configuration_value( - [ - { - "key": "EMAIL_HOST", - "default": os.environ.get("EMAIL_HOST"), - }, - { - "key": "EMAIL_HOST_USER", - "default": os.environ.get("EMAIL_HOST_USER"), - }, - { - "key": "EMAIL_HOST_PASSWORD", - "default": os.environ.get("EMAIL_HOST_PASSWORD"), - }, - { - "key": "EMAIL_PORT", - "default": os.environ.get("EMAIL_PORT", 587), - }, - { - "key": "EMAIL_USE_TLS", - "default": os.environ.get("EMAIL_USE_TLS", "1"), - }, - { - "key": "EMAIL_FROM", - "default": os.environ.get("EMAIL_FROM", "Team Plane "), - }, - ] - ) + return get_configuration_value( + [ + { + "key": "EMAIL_HOST", + "default": os.environ.get("EMAIL_HOST"), + }, + { + "key": "EMAIL_HOST_USER", + "default": os.environ.get("EMAIL_HOST_USER"), + }, + { + "key": "EMAIL_HOST_PASSWORD", + "default": os.environ.get("EMAIL_HOST_PASSWORD"), + }, + { + "key": "EMAIL_PORT", + "default": os.environ.get("EMAIL_PORT", 587), + }, + { + "key": "EMAIL_USE_TLS", + "default": os.environ.get("EMAIL_USE_TLS", "1"), + }, + { + "key": "EMAIL_FROM", + "default": os.environ.get( + "EMAIL_FROM", "Team Plane " + ), + }, + ] ) - diff --git a/apiserver/plane/middleware/api_log_middleware.py b/apiserver/plane/middleware/api_log_middleware.py index a1894fad5..a49d43b55 100644 --- a/apiserver/plane/middleware/api_log_middleware.py +++ b/apiserver/plane/middleware/api_log_middleware.py @@ -23,9 +23,13 @@ class APITokenLogMiddleware: method=request.method, query_params=request.META.get("QUERY_STRING", ""), headers=str(request.headers), - body=(request_body.decode('utf-8') if request_body else None), + body=( + request_body.decode("utf-8") if request_body else None + ), response_body=( - response.content.decode("utf-8") if response.content else None + response.content.decode("utf-8") + if response.content + else None ), response_code=response.status_code, ip_address=request.META.get("REMOTE_ADDR", None), diff --git a/apiserver/plane/middleware/apps.py b/apiserver/plane/middleware/apps.py index 3da4958c1..9deac8091 100644 --- a/apiserver/plane/middleware/apps.py +++ b/apiserver/plane/middleware/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class Middleware(AppConfig): - name = 'plane.middleware' + name = "plane.middleware" diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 971ed5543..623583840 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -71,13 +71,19 @@ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", ), - "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), + "DEFAULT_PERMISSION_CLASSES": ( + "rest_framework.permissions.IsAuthenticated", + ), "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), - "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), + "DEFAULT_FILTER_BACKENDS": ( + "django_filters.rest_framework.DjangoFilterBackend", + ), } # Django Auth Backend -AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) # default +AUTHENTICATION_BACKENDS = ( + "django.contrib.auth.backends.ModelBackend", +) # default # Root Urls ROOT_URLCONF = "plane.urls" @@ -229,9 +235,9 @@ AWS_REGION = os.environ.get("AWS_REGION", "") AWS_DEFAULT_ACL = "public-read" AWS_QUERYSTRING_AUTH = False AWS_S3_FILE_OVERWRITE = False -AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", None) or os.environ.get( - "MINIO_ENDPOINT_URL", None -) +AWS_S3_ENDPOINT_URL = os.environ.get( + "AWS_S3_ENDPOINT_URL", None +) or os.environ.get("MINIO_ENDPOINT_URL", None) if AWS_S3_ENDPOINT_URL: parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost")) AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}" @@ -274,9 +280,7 @@ CELERY_ACCEPT_CONTENT = ["application/json"] if REDIS_SSL: redis_url = os.environ.get("REDIS_URL") - broker_url = ( - f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" - ) + broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" CELERY_BROKER_URL = broker_url CELERY_RESULT_BACKEND = broker_url else: @@ -310,7 +314,7 @@ if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get( # Application Envs PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) # For External -SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False) + FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) # Unsplash Access key @@ -331,7 +335,8 @@ POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False) # instance key INSTANCE_KEY = os.environ.get( - "INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3" + "INSTANCE_KEY", + "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3", ) # Skip environment variable configuration diff --git a/apiserver/plane/settings/test.py b/apiserver/plane/settings/test.py index 34ae16555..1e2a55144 100644 --- a/apiserver/plane/settings/test.py +++ b/apiserver/plane/settings/test.py @@ -1,9 +1,11 @@ """Test Settings""" -from .common import * # noqa +from .common import * # noqa DEBUG = True # Send it in a dummy outbox EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" -INSTALLED_APPS.append("plane.tests",) +INSTALLED_APPS.append( + "plane.tests", +) diff --git a/apiserver/plane/space/serializer/base.py b/apiserver/plane/space/serializer/base.py index 89c9725d9..4b92b06fc 100644 --- a/apiserver/plane/space/serializer/base.py +++ b/apiserver/plane/space/serializer/base.py @@ -4,8 +4,8 @@ from rest_framework import serializers class BaseSerializer(serializers.ModelSerializer): id = serializers.PrimaryKeyRelatedField(read_only=True) -class DynamicBaseSerializer(BaseSerializer): +class DynamicBaseSerializer(BaseSerializer): def __init__(self, *args, **kwargs): # If 'fields' is provided in the arguments, remove it and store it separately. # This is done so as not to pass this custom argument up to the superclass. @@ -31,7 +31,7 @@ class DynamicBaseSerializer(BaseSerializer): # loop through its keys and values. if isinstance(field_name, dict): for key, value in field_name.items(): - # If the value of this nested field is a list, + # If the value of this nested field is a list, # perform a recursive filter on it. if isinstance(value, list): self._filter_fields(self.fields[key], value) @@ -52,7 +52,7 @@ class DynamicBaseSerializer(BaseSerializer): allowed = set(allowed) # Remove fields from the serializer that aren't in the 'allowed' list. - for field_name in (existing - allowed): + for field_name in existing - allowed: self.fields.pop(field_name) return self.fields diff --git a/apiserver/plane/space/serializer/cycle.py b/apiserver/plane/space/serializer/cycle.py index ab4d9441d..d4f5d86e0 100644 --- a/apiserver/plane/space/serializer/cycle.py +++ b/apiserver/plane/space/serializer/cycle.py @@ -4,6 +4,7 @@ from plane.db.models import ( Cycle, ) + class CycleBaseSerializer(BaseSerializer): class Meta: model = Cycle @@ -15,4 +16,4 @@ class CycleBaseSerializer(BaseSerializer): "updated_by", "created_at", "updated_at", - ] \ No newline at end of file + ] diff --git a/apiserver/plane/space/serializer/inbox.py b/apiserver/plane/space/serializer/inbox.py index 05d99ac55..48ec7c89d 100644 --- a/apiserver/plane/space/serializer/inbox.py +++ b/apiserver/plane/space/serializer/inbox.py @@ -36,12 +36,16 @@ class InboxIssueLiteSerializer(BaseSerializer): class IssueStateInboxSerializer(BaseSerializer): state_detail = StateLiteSerializer(read_only=True, source="state") project_detail = ProjectLiteSerializer(read_only=True, source="project") - label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) - assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) + label_details = LabelLiteSerializer( + read_only=True, source="labels", many=True + ) + assignee_details = UserLiteSerializer( + read_only=True, source="assignees", many=True + ) sub_issues_count = serializers.IntegerField(read_only=True) bridge_id = serializers.UUIDField(read_only=True) issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True) class Meta: model = Issue - fields = "__all__" \ No newline at end of file + fields = "__all__" diff --git a/apiserver/plane/space/serializer/issue.py b/apiserver/plane/space/serializer/issue.py index 1a9a872ef..c7b044b21 100644 --- a/apiserver/plane/space/serializer/issue.py +++ b/apiserver/plane/space/serializer/issue.py @@ -1,4 +1,3 @@ - # Django imports from django.utils import timezone @@ -47,7 +46,9 @@ class IssueStateFlatSerializer(BaseSerializer): class LabelSerializer(BaseSerializer): - workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + workspace_detail = WorkspaceLiteSerializer( + source="workspace", read_only=True + ) project_detail = ProjectLiteSerializer(source="project", read_only=True) class Meta: @@ -74,7 +75,9 @@ class IssueProjectLiteSerializer(BaseSerializer): class IssueRelationSerializer(BaseSerializer): - issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue") + issue_detail = IssueProjectLiteSerializer( + read_only=True, source="related_issue" + ) class Meta: model = IssueRelation @@ -83,13 +86,14 @@ class IssueRelationSerializer(BaseSerializer): "relation_type", "related_issue", "issue", - "id" + "id", ] read_only_fields = [ "workspace", "project", ] + class RelatedIssueSerializer(BaseSerializer): issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue") @@ -100,7 +104,7 @@ class RelatedIssueSerializer(BaseSerializer): "relation_type", "related_issue", "issue", - "id" + "id", ] read_only_fields = [ "workspace", @@ -159,7 +163,8 @@ class IssueLinkSerializer(BaseSerializer): # Validation if url already exists def create(self, validated_data): if IssueLink.objects.filter( - url=validated_data.get("url"), issue_id=validated_data.get("issue_id") + url=validated_data.get("url"), + issue_id=validated_data.get("issue_id"), ).exists(): raise serializers.ValidationError( {"error": "URL already exists for this Issue"} @@ -183,9 +188,8 @@ class IssueAttachmentSerializer(BaseSerializer): class IssueReactionSerializer(BaseSerializer): - actor_detail = UserLiteSerializer(read_only=True, source="actor") - + class Meta: model = IssueReaction fields = "__all__" @@ -202,9 +206,15 @@ class IssueSerializer(BaseSerializer): state_detail = StateSerializer(read_only=True, source="state") parent_detail = IssueStateFlatSerializer(read_only=True, source="parent") label_details = LabelSerializer(read_only=True, source="labels", many=True) - assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) - related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True) - issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True) + assignee_details = UserLiteSerializer( + read_only=True, source="assignees", many=True + ) + related_issues = IssueRelationSerializer( + read_only=True, source="issue_relation", many=True + ) + issue_relations = RelatedIssueSerializer( + read_only=True, source="issue_related", many=True + ) issue_cycle = IssueCycleDetailSerializer(read_only=True) issue_module = IssueModuleDetailSerializer(read_only=True) issue_link = IssueLinkSerializer(read_only=True, many=True) @@ -261,8 +271,12 @@ class IssueCommentSerializer(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") - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") - comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True) + workspace_detail = WorkspaceLiteSerializer( + read_only=True, source="workspace" + ) + comment_reactions = CommentReactionLiteSerializer( + read_only=True, many=True + ) is_member = serializers.BooleanField(read_only=True) class Meta: @@ -285,7 +299,9 @@ class IssueCreateSerializer(BaseSerializer): state_detail = StateSerializer(read_only=True, source="state") created_by_detail = UserLiteSerializer(read_only=True, source="created_by") project_detail = ProjectLiteSerializer(read_only=True, source="project") - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + workspace_detail = WorkspaceLiteSerializer( + read_only=True, source="workspace" + ) assignees = serializers.ListField( child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), @@ -313,8 +329,10 @@ class IssueCreateSerializer(BaseSerializer): def to_representation(self, instance): data = super().to_representation(instance) - data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()] - data['labels'] = [str(label.id) for label in instance.labels.all()] + data["assignees"] = [ + str(assignee.id) for assignee in instance.assignees.all() + ] + data["labels"] = [str(label.id) for label in instance.labels.all()] return data def validate(self, data): @@ -323,7 +341,9 @@ class IssueCreateSerializer(BaseSerializer): and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None) ): - raise serializers.ValidationError("Start date cannot exceed target date") + raise serializers.ValidationError( + "Start date cannot exceed target date" + ) return data def create(self, validated_data): @@ -432,12 +452,11 @@ class IssueCreateSerializer(BaseSerializer): # Time updation occues even when other related models are updated instance.updated_at = timezone.now() return super().update(instance, validated_data) - + class IssueReactionSerializer(BaseSerializer): - actor_detail = UserLiteSerializer(read_only=True, source="actor") - + class Meta: model = IssueReaction fields = "__all__" @@ -457,19 +476,27 @@ class CommentReactionSerializer(BaseSerializer): class IssueVoteSerializer(BaseSerializer): - actor_detail = UserLiteSerializer(read_only=True, source="actor") class Meta: model = IssueVote - fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"] + fields = [ + "issue", + "vote", + "workspace", + "project", + "actor", + "actor_detail", + ] read_only_fields = fields class IssuePublicSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state") - reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions") + reactions = IssueReactionSerializer( + read_only=True, many=True, source="issue_reactions" + ) votes = IssueVoteSerializer(read_only=True, many=True) class Meta: @@ -500,7 +527,3 @@ class LabelLiteSerializer(BaseSerializer): "name", "color", ] - - - - diff --git a/apiserver/plane/space/serializer/module.py b/apiserver/plane/space/serializer/module.py index 39ce9ec32..dda1861d1 100644 --- a/apiserver/plane/space/serializer/module.py +++ b/apiserver/plane/space/serializer/module.py @@ -4,6 +4,7 @@ from plane.db.models import ( Module, ) + class ModuleBaseSerializer(BaseSerializer): class Meta: model = Module @@ -15,4 +16,4 @@ class ModuleBaseSerializer(BaseSerializer): "updated_by", "created_at", "updated_at", - ] \ No newline at end of file + ] diff --git a/apiserver/plane/space/serializer/state.py b/apiserver/plane/space/serializer/state.py index 903bcc2f4..55064ed0e 100644 --- a/apiserver/plane/space/serializer/state.py +++ b/apiserver/plane/space/serializer/state.py @@ -6,7 +6,6 @@ from plane.db.models import ( class StateSerializer(BaseSerializer): - class Meta: model = State fields = "__all__" diff --git a/apiserver/plane/space/serializer/workspace.py b/apiserver/plane/space/serializer/workspace.py index ecf99079f..a31bb3744 100644 --- a/apiserver/plane/space/serializer/workspace.py +++ b/apiserver/plane/space/serializer/workspace.py @@ -4,6 +4,7 @@ from plane.db.models import ( Workspace, ) + class WorkspaceLiteSerializer(BaseSerializer): class Meta: model = Workspace @@ -12,4 +13,4 @@ class WorkspaceLiteSerializer(BaseSerializer): "slug", "id", ] - read_only_fields = fields \ No newline at end of file + read_only_fields = fields diff --git a/apiserver/plane/space/views/base.py b/apiserver/plane/space/views/base.py index b1d749a09..b75f3dd18 100644 --- a/apiserver/plane/space/views/base.py +++ b/apiserver/plane/space/views/base.py @@ -59,7 +59,9 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): return self.model.objects.all() except Exception as e: capture_exception(e) - raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST) + raise APIException( + "Please check the view", status.HTTP_400_BAD_REQUEST + ) def handle_exception(self, exc): """ @@ -83,23 +85,27 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): ) if isinstance(e, ObjectDoesNotExist): - model_name = str(exc).split(" matching query does not exist.")[0] + model_name = str(exc).split(" matching query does not exist.")[ + 0 + ] return Response( - {"error": f"{model_name} does not exist."}, + {"error": f"The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) if isinstance(e, KeyError): capture_exception(e) return Response( - {"error": f"key {e} does not exist"}, + {"error": "The required key does not exist."}, status=status.HTTP_400_BAD_REQUEST, ) - + print(e) if settings.DEBUG else print("Server Error") capture_exception(e) - return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) def dispatch(self, request, *args, **kwargs): try: @@ -172,20 +178,24 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): ) if isinstance(e, ObjectDoesNotExist): - model_name = str(exc).split(" matching query does not exist.")[0] return Response( - {"error": f"{model_name} does not exist."}, + {"error": f"The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) - + if isinstance(e, KeyError): - return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": "The required key does not exist."}, + status=status.HTTP_400_BAD_REQUEST, + ) if settings.DEBUG: print(e) capture_exception(e) - return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) def dispatch(self, request, *args, **kwargs): try: diff --git a/apiserver/plane/space/views/inbox.py b/apiserver/plane/space/views/inbox.py index 53960f672..2bf8f8303 100644 --- a/apiserver/plane/space/views/inbox.py +++ b/apiserver/plane/space/views/inbox.py @@ -48,7 +48,8 @@ class InboxIssuePublicViewSet(BaseViewSet): super() .get_queryset() .filter( - Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + Q(snoozed_till__gte=timezone.now()) + | Q(snoozed_till__isnull=True), project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), inbox_id=self.kwargs.get("inbox_id"), @@ -80,7 +81,9 @@ class InboxIssuePublicViewSet(BaseViewSet): .prefetch_related("assignees", "labels") .order_by("issue_inbox__snoozed_till", "issue_inbox__status") .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -92,7 +95,9 @@ class InboxIssuePublicViewSet(BaseViewSet): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -124,7 +129,8 @@ class InboxIssuePublicViewSet(BaseViewSet): if not request.data.get("issue", {}).get("name", False): return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, ) # Check for valid priority @@ -136,7 +142,8 @@ class InboxIssuePublicViewSet(BaseViewSet): "none", ]: return Response( - {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Invalid priority"}, + status=status.HTTP_400_BAD_REQUEST, ) # Create or get state @@ -192,7 +199,10 @@ class InboxIssuePublicViewSet(BaseViewSet): ) inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + pk=pk, + workspace__slug=slug, + project_id=project_id, + inbox_id=inbox_id, ) # Get the project member if str(inbox_issue.created_by_id) != str(request.user.id): @@ -205,7 +215,9 @@ class InboxIssuePublicViewSet(BaseViewSet): issue_data = request.data.pop("issue", False) issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + pk=inbox_issue.issue_id, + workspace__slug=slug, + project_id=project_id, ) # viewers and guests since only viewers and guests issue_data = { @@ -216,7 +228,9 @@ class InboxIssuePublicViewSet(BaseViewSet): "description": issue_data.get("description", issue.description), } - issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True) + issue_serializer = IssueCreateSerializer( + issue, data=issue_data, partial=True + ) if issue_serializer.is_valid(): current_instance = issue @@ -237,7 +251,9 @@ class InboxIssuePublicViewSet(BaseViewSet): ) issue_serializer.save() return Response(issue_serializer.data, status=status.HTTP_200_OK) - return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) def retrieve(self, request, slug, project_id, inbox_id, pk): project_deploy_board = ProjectDeployBoard.objects.get( @@ -250,10 +266,15 @@ class InboxIssuePublicViewSet(BaseViewSet): ) inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + pk=pk, + workspace__slug=slug, + project_id=project_id, + inbox_id=inbox_id, ) issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + pk=inbox_issue.issue_id, + workspace__slug=slug, + project_id=project_id, ) serializer = IssueStateInboxSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) @@ -269,7 +290,10 @@ class InboxIssuePublicViewSet(BaseViewSet): ) inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + pk=pk, + workspace__slug=slug, + project_id=project_id, + inbox_id=inbox_id, ) if str(inbox_issue.created_by_id) != str(request.user.id): diff --git a/apiserver/plane/space/views/issue.py b/apiserver/plane/space/views/issue.py index faab8834d..8f7fc0eaa 100644 --- a/apiserver/plane/space/views/issue.py +++ b/apiserver/plane/space/views/issue.py @@ -128,7 +128,9 @@ class IssueCommentPublicViewSet(BaseViewSet): ) issue_activity.delay( type="comment.activity.created", - requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), actor_id=str(request.user.id), issue_id=str(issue_id), project_id=str(project_id), @@ -162,7 +164,9 @@ class IssueCommentPublicViewSet(BaseViewSet): comment = IssueComment.objects.get( workspace__slug=slug, pk=pk, actor=request.user ) - serializer = IssueCommentSerializer(comment, data=request.data, partial=True) + serializer = IssueCommentSerializer( + comment, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() issue_activity.delay( @@ -191,7 +195,10 @@ class IssueCommentPublicViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) comment = IssueComment.objects.get( - workspace__slug=slug, pk=pk, project_id=project_id, actor=request.user + workspace__slug=slug, + pk=pk, + project_id=project_id, + actor=request.user, ) issue_activity.delay( type="comment.activity.deleted", @@ -261,7 +268,9 @@ class IssueReactionPublicViewSet(BaseViewSet): ) issue_activity.delay( type="issue_reaction.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), 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)), @@ -343,7 +352,9 @@ class CommentReactionPublicViewSet(BaseViewSet): serializer = CommentReactionSerializer(data=request.data) if serializer.is_valid(): serializer.save( - project_id=project_id, comment_id=comment_id, actor=request.user + project_id=project_id, + comment_id=comment_id, + actor=request.user, ) if not ProjectMember.objects.filter( project_id=project_id, @@ -357,7 +368,9 @@ class CommentReactionPublicViewSet(BaseViewSet): ) issue_activity.delay( type="comment_reaction.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), actor_id=str(self.request.user.id), issue_id=None, project_id=str(self.kwargs.get("project_id", None)), @@ -445,7 +458,9 @@ class IssueVotePublicViewSet(BaseViewSet): issue_vote.save() issue_activity.delay( type="issue_vote.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), 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)), @@ -507,13 +522,21 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] order_by_param = request.GET.get("order_by", "-created_at") issue_queryset = ( Issue.issue_objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -544,7 +567,9 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): .values("count") ) .annotate( - attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -554,7 +579,9 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( - priority_order if order_by_param == "priority" else priority_order[::-1] + priority_order + if order_by_param == "priority" + else priority_order[::-1] ) issue_queryset = issue_queryset.annotate( priority_order=Case( @@ -602,7 +629,9 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): else order_by_param ) ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + "-max_values" + if order_by_param.startswith("-") + else "max_values" ) else: issue_queryset = issue_queryset.order_by(order_by_param) @@ -653,4 +682,4 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): "labels": labels, }, status=status.HTTP_200_OK, - ) \ No newline at end of file + ) diff --git a/apiserver/plane/tests/api/base.py b/apiserver/plane/tests/api/base.py index e3209a281..f6843c1b6 100644 --- a/apiserver/plane/tests/api/base.py +++ b/apiserver/plane/tests/api/base.py @@ -8,7 +8,9 @@ from plane.app.views.authentication import get_tokens_for_user class BaseAPITest(APITestCase): def setUp(self): - self.client = APIClient(HTTP_USER_AGENT="plane/test", REMOTE_ADDR="10.10.10.10") + self.client = APIClient( + HTTP_USER_AGENT="plane/test", REMOTE_ADDR="10.10.10.10" + ) class AuthenticatedAPITest(BaseAPITest): diff --git a/apiserver/plane/tests/api/test_asset.py b/apiserver/plane/tests/api/test_asset.py index 51a36ba2f..b15d32e40 100644 --- a/apiserver/plane/tests/api/test_asset.py +++ b/apiserver/plane/tests/api/test_asset.py @@ -1 +1 @@ -# TODO: Tests for File Asset Uploads \ No newline at end of file +# TODO: Tests for File Asset Uploads diff --git a/apiserver/plane/tests/api/test_auth_extended.py b/apiserver/plane/tests/api/test_auth_extended.py index 92ad92d6e..af6450ef4 100644 --- a/apiserver/plane/tests/api/test_auth_extended.py +++ b/apiserver/plane/tests/api/test_auth_extended.py @@ -1 +1 @@ -#TODO: Tests for ChangePassword and other Endpoints \ No newline at end of file +# TODO: Tests for ChangePassword and other Endpoints diff --git a/apiserver/plane/tests/api/test_authentication.py b/apiserver/plane/tests/api/test_authentication.py index 4fc46e008..36a0f7a24 100644 --- a/apiserver/plane/tests/api/test_authentication.py +++ b/apiserver/plane/tests/api/test_authentication.py @@ -21,16 +21,16 @@ class SignInEndpointTests(BaseAPITest): user.save() def test_without_data(self): - url = reverse("sign-in") response = self.client.post(url, {}, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_email_validity(self): - url = reverse("sign-in") response = self.client.post( - url, {"email": "useremail.com", "password": "user@123"}, format="json" + url, + {"email": "useremail.com", "password": "user@123"}, + format="json", ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( @@ -40,7 +40,9 @@ class SignInEndpointTests(BaseAPITest): def test_password_validity(self): url = reverse("sign-in") response = self.client.post( - url, {"email": "user@plane.so", "password": "user123"}, format="json" + url, + {"email": "user@plane.so", "password": "user123"}, + format="json", ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual( @@ -53,7 +55,9 @@ class SignInEndpointTests(BaseAPITest): def test_user_exists(self): url = reverse("sign-in") response = self.client.post( - url, {"email": "user@email.so", "password": "user123"}, format="json" + url, + {"email": "user@email.so", "password": "user123"}, + format="json", ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual( @@ -87,15 +91,15 @@ class MagicLinkGenerateEndpointTests(BaseAPITest): user.save() def test_without_data(self): - url = reverse("magic-generate") response = self.client.post(url, {}, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_email_validity(self): - url = reverse("magic-generate") - response = self.client.post(url, {"email": "useremail.com"}, format="json") + response = self.client.post( + url, {"email": "useremail.com"}, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( response.data, {"error": "Please provide a valid email address."} @@ -107,7 +111,9 @@ class MagicLinkGenerateEndpointTests(BaseAPITest): ri = redis_instance() ri.delete("magic_user@plane.so") - response = self.client.post(url, {"email": "user@plane.so"}, format="json") + response = self.client.post( + url, {"email": "user@plane.so"}, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_max_generate_attempt(self): @@ -131,7 +137,8 @@ class MagicLinkGenerateEndpointTests(BaseAPITest): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - response.data, {"error": "Max attempts exhausted. Please try again later."} + response.data, + {"error": "Max attempts exhausted. Please try again later."}, ) @@ -143,14 +150,14 @@ class MagicSignInEndpointTests(BaseAPITest): user.save() def test_without_data(self): - url = reverse("magic-sign-in") response = self.client.post(url, {}, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {"error": "User token and key are required"}) + self.assertEqual( + response.data, {"error": "User token and key are required"} + ) def test_expired_invalid_magic_link(self): - ri = redis_instance() ri.delete("magic_user@plane.so") @@ -162,11 +169,11 @@ class MagicSignInEndpointTests(BaseAPITest): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - response.data, {"error": "The magic code/link has expired please try again"} + response.data, + {"error": "The magic code/link has expired please try again"}, ) def test_invalid_magic_code(self): - ri = redis_instance() ri.delete("magic_user@plane.so") ## Create Token @@ -181,11 +188,11 @@ class MagicSignInEndpointTests(BaseAPITest): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - response.data, {"error": "Your login code was incorrect. Please try again."} + response.data, + {"error": "Your login code was incorrect. Please try again."}, ) def test_magic_code_sign_in(self): - ri = redis_instance() ri.delete("magic_user@plane.so") ## Create Token diff --git a/apiserver/plane/tests/api/test_cycle.py b/apiserver/plane/tests/api/test_cycle.py index 04c2d6ba2..72b580c99 100644 --- a/apiserver/plane/tests/api/test_cycle.py +++ b/apiserver/plane/tests/api/test_cycle.py @@ -1 +1 @@ -# TODO: Write Test for Cycle Endpoints \ No newline at end of file +# TODO: Write Test for Cycle Endpoints diff --git a/apiserver/plane/tests/api/test_issue.py b/apiserver/plane/tests/api/test_issue.py index 3e59613e0..a45ff36b1 100644 --- a/apiserver/plane/tests/api/test_issue.py +++ b/apiserver/plane/tests/api/test_issue.py @@ -1 +1 @@ -# TODO: Write Test for Issue Endpoints \ No newline at end of file +# TODO: Write Test for Issue Endpoints diff --git a/apiserver/plane/tests/api/test_oauth.py b/apiserver/plane/tests/api/test_oauth.py index e70e4fccb..1e7dac0ef 100644 --- a/apiserver/plane/tests/api/test_oauth.py +++ b/apiserver/plane/tests/api/test_oauth.py @@ -1 +1 @@ -#TODO: Tests for OAuth Authentication Endpoint \ No newline at end of file +# TODO: Tests for OAuth Authentication Endpoint diff --git a/apiserver/plane/tests/api/test_people.py b/apiserver/plane/tests/api/test_people.py index c4750f9b8..624281a2f 100644 --- a/apiserver/plane/tests/api/test_people.py +++ b/apiserver/plane/tests/api/test_people.py @@ -1 +1 @@ -# TODO: Write Test for people Endpoint \ No newline at end of file +# TODO: Write Test for people Endpoint diff --git a/apiserver/plane/tests/api/test_project.py b/apiserver/plane/tests/api/test_project.py index 49dae5581..9a7c50f19 100644 --- a/apiserver/plane/tests/api/test_project.py +++ b/apiserver/plane/tests/api/test_project.py @@ -1 +1 @@ -# TODO: Write Tests for project endpoints \ No newline at end of file +# TODO: Write Tests for project endpoints diff --git a/apiserver/plane/tests/api/test_shortcut.py b/apiserver/plane/tests/api/test_shortcut.py index 2e939af70..5103b5059 100644 --- a/apiserver/plane/tests/api/test_shortcut.py +++ b/apiserver/plane/tests/api/test_shortcut.py @@ -1 +1 @@ -# TODO: Write Test for shortcuts \ No newline at end of file +# TODO: Write Test for shortcuts diff --git a/apiserver/plane/tests/api/test_state.py b/apiserver/plane/tests/api/test_state.py index ef9631bc2..a336d955a 100644 --- a/apiserver/plane/tests/api/test_state.py +++ b/apiserver/plane/tests/api/test_state.py @@ -1 +1 @@ -# TODO: Wrote test for state endpoints \ No newline at end of file +# TODO: Wrote test for state endpoints diff --git a/apiserver/plane/tests/api/test_workspace.py b/apiserver/plane/tests/api/test_workspace.py index a1da2997a..c1e487fbe 100644 --- a/apiserver/plane/tests/api/test_workspace.py +++ b/apiserver/plane/tests/api/test_workspace.py @@ -14,7 +14,6 @@ class WorkSpaceCreateReadUpdateDelete(AuthenticatedAPITest): super().setUp() def test_create_workspace(self): - url = reverse("workspace") # Test with empty data @@ -32,7 +31,9 @@ class WorkSpaceCreateReadUpdateDelete(AuthenticatedAPITest): # Check other values workspace = Workspace.objects.get(pk=response.data["id"]) - workspace_member = WorkspaceMember.objects.get(workspace=workspace, member_id=self.user_id) + workspace_member = WorkspaceMember.objects.get( + workspace=workspace, member_id=self.user_id + ) self.assertEqual(workspace.owner_id, self.user_id) self.assertEqual(workspace_member.role, 20) diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index e437da078..669f3ea73 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -12,7 +12,7 @@ urlpatterns = [ path("", TemplateView.as_view(template_name="index.html")), path("api/", include("plane.app.urls")), path("api/public/", include("plane.space.urls")), - path("api/licenses/", include("plane.license.urls")), + path("api/instances/", include("plane.license.urls")), path("api/v1/", include("plane.api.urls")), path("", include("plane.web.urls")), ] diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index be52bcce4..07d456a1d 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -6,7 +6,12 @@ from datetime import timedelta from django.db import models from django.db.models.functions import TruncDate from django.db.models import Count, F, Sum, Value, Case, When, CharField -from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear, Concat +from django.db.models.functions import ( + Coalesce, + ExtractMonth, + ExtractYear, + Concat, +) # Module imports from plane.db.models import Issue @@ -21,14 +26,18 @@ def annotate_with_monthly_dimension(queryset, field_name, attribute): # Annotate the dimension return queryset.annotate(**{attribute: dimension}) + def extract_axis(queryset, x_axis): # Format the dimension when the axis is in date if x_axis in ["created_at", "start_date", "target_date", "completed_at"]: - queryset = annotate_with_monthly_dimension(queryset, x_axis, "dimension") + queryset = annotate_with_monthly_dimension( + queryset, x_axis, "dimension" + ) return queryset, "dimension" else: return queryset.annotate(dimension=F(x_axis)), "dimension" + def sort_data(data, temp_axis): # When the axis is in priority order by if temp_axis == "priority": @@ -37,6 +46,7 @@ def sort_data(data, temp_axis): else: return dict(sorted(data.items(), key=lambda x: (x[0] == "none", x[0]))) + def build_graph_plot(queryset, x_axis, y_axis, segment=None): # temp x_axis temp_axis = x_axis @@ -45,9 +55,11 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None): if x_axis == "dimension": queryset = queryset.exclude(dimension__isnull=True) - # + # if segment in ["created_at", "start_date", "target_date", "completed_at"]: - queryset = annotate_with_monthly_dimension(queryset, segment, "segmented") + queryset = annotate_with_monthly_dimension( + queryset, segment, "segmented" + ) segment = "segmented" queryset = queryset.values(x_axis) @@ -62,21 +74,41 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None): ), dimension_ex=Coalesce("dimension", Value("null")), ).values("dimension") - queryset = queryset.annotate(segment=F(segment)) if segment else queryset - queryset = queryset.values("dimension", "segment") if segment else queryset.values("dimension") + queryset = ( + queryset.annotate(segment=F(segment)) if segment else queryset + ) + queryset = ( + queryset.values("dimension", "segment") + if segment + else queryset.values("dimension") + ) queryset = queryset.annotate(count=Count("*")).order_by("dimension") # Estimate else: - queryset = queryset.annotate(estimate=Sum("estimate_point")).order_by(x_axis) - queryset = queryset.annotate(segment=F(segment)) if segment else queryset - queryset = queryset.values("dimension", "segment", "estimate") if segment else queryset.values("dimension", "estimate") + queryset = queryset.annotate(estimate=Sum("estimate_point")).order_by( + x_axis + ) + queryset = ( + queryset.annotate(segment=F(segment)) if segment else queryset + ) + queryset = ( + queryset.values("dimension", "segment", "estimate") + if segment + else queryset.values("dimension", "estimate") + ) result_values = list(queryset) - grouped_data = {str(key): list(items) for key, items in groupby(result_values, key=lambda x: x[str("dimension")])} + grouped_data = { + str(key): list(items) + for key, items in groupby( + result_values, key=lambda x: x[str("dimension")] + ) + } return sort_data(grouped_data, temp_axis) + def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): # Total Issues in Cycle or Module total_issues = queryset.total_issues @@ -107,7 +139,9 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): # Get all dates between the two dates date_range = [ queryset.start_date + timedelta(days=x) - for x in range((queryset.target_date - queryset.start_date).days + 1) + for x in range( + (queryset.target_date - queryset.start_date).days + 1 + ) ] chart_data = {str(date): 0 for date in date_range} diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index 853874b31..edc7adc15 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -40,77 +40,144 @@ def group_results(results_data, group_by, sub_group_by=False): for value in results_data: main_group_attribute = resolve_keys(sub_group_by, value) group_attribute = resolve_keys(group_by, value) - if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list): + if isinstance(main_group_attribute, list) and not isinstance( + group_attribute, list + ): if len(main_group_attribute): for attrib in main_group_attribute: if str(attrib) not in main_responsive_dict: main_responsive_dict[str(attrib)] = {} - if str(group_attribute) in main_responsive_dict[str(attrib)]: - main_responsive_dict[str(attrib)][str(group_attribute)].append(value) + if ( + str(group_attribute) + in main_responsive_dict[str(attrib)] + ): + main_responsive_dict[str(attrib)][ + str(group_attribute) + ].append(value) else: - main_responsive_dict[str(attrib)][str(group_attribute)] = [] - main_responsive_dict[str(attrib)][str(group_attribute)].append(value) + main_responsive_dict[str(attrib)][ + str(group_attribute) + ] = [] + main_responsive_dict[str(attrib)][ + str(group_attribute) + ].append(value) else: if str(None) not in main_responsive_dict: main_responsive_dict[str(None)] = {} if str(group_attribute) in main_responsive_dict[str(None)]: - main_responsive_dict[str(None)][str(group_attribute)].append(value) + main_responsive_dict[str(None)][ + str(group_attribute) + ].append(value) else: - main_responsive_dict[str(None)][str(group_attribute)] = [] - main_responsive_dict[str(None)][str(group_attribute)].append(value) + main_responsive_dict[str(None)][ + str(group_attribute) + ] = [] + main_responsive_dict[str(None)][ + str(group_attribute) + ].append(value) - elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list): + elif isinstance(group_attribute, list) and not isinstance( + main_group_attribute, list + ): if str(main_group_attribute) not in main_responsive_dict: main_responsive_dict[str(main_group_attribute)] = {} if len(group_attribute): for attrib in group_attribute: - if str(attrib) in main_responsive_dict[str(main_group_attribute)]: - main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value) + if ( + str(attrib) + in main_responsive_dict[str(main_group_attribute)] + ): + main_responsive_dict[str(main_group_attribute)][ + str(attrib) + ].append(value) else: - main_responsive_dict[str(main_group_attribute)][str(attrib)] = [] - main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value) + main_responsive_dict[str(main_group_attribute)][ + str(attrib) + ] = [] + main_responsive_dict[str(main_group_attribute)][ + str(attrib) + ].append(value) else: - if str(None) in main_responsive_dict[str(main_group_attribute)]: - main_responsive_dict[str(main_group_attribute)][str(None)].append(value) + if ( + str(None) + in main_responsive_dict[str(main_group_attribute)] + ): + main_responsive_dict[str(main_group_attribute)][ + str(None) + ].append(value) else: - main_responsive_dict[str(main_group_attribute)][str(None)] = [] - main_responsive_dict[str(main_group_attribute)][str(None)].append(value) + main_responsive_dict[str(main_group_attribute)][ + str(None) + ] = [] + main_responsive_dict[str(main_group_attribute)][ + str(None) + ].append(value) - elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list): + elif isinstance(group_attribute, list) and isinstance( + main_group_attribute, list + ): if len(main_group_attribute): for main_attrib in main_group_attribute: if str(main_attrib) not in main_responsive_dict: main_responsive_dict[str(main_attrib)] = {} if len(group_attribute): for attrib in group_attribute: - if str(attrib) in main_responsive_dict[str(main_attrib)]: - main_responsive_dict[str(main_attrib)][str(attrib)].append(value) + if ( + str(attrib) + in main_responsive_dict[str(main_attrib)] + ): + main_responsive_dict[str(main_attrib)][ + str(attrib) + ].append(value) else: - main_responsive_dict[str(main_attrib)][str(attrib)] = [] - main_responsive_dict[str(main_attrib)][str(attrib)].append(value) + main_responsive_dict[str(main_attrib)][ + str(attrib) + ] = [] + main_responsive_dict[str(main_attrib)][ + str(attrib) + ].append(value) else: - if str(None) in main_responsive_dict[str(main_attrib)]: - main_responsive_dict[str(main_attrib)][str(None)].append(value) + if ( + str(None) + in main_responsive_dict[str(main_attrib)] + ): + main_responsive_dict[str(main_attrib)][ + str(None) + ].append(value) else: - main_responsive_dict[str(main_attrib)][str(None)] = [] - main_responsive_dict[str(main_attrib)][str(None)].append(value) + main_responsive_dict[str(main_attrib)][ + str(None) + ] = [] + main_responsive_dict[str(main_attrib)][ + str(None) + ].append(value) else: if str(None) not in main_responsive_dict: main_responsive_dict[str(None)] = {} if len(group_attribute): for attrib in group_attribute: if str(attrib) in main_responsive_dict[str(None)]: - main_responsive_dict[str(None)][str(attrib)].append(value) + main_responsive_dict[str(None)][ + str(attrib) + ].append(value) else: - main_responsive_dict[str(None)][str(attrib)] = [] - main_responsive_dict[str(None)][str(attrib)].append(value) + main_responsive_dict[str(None)][ + str(attrib) + ] = [] + main_responsive_dict[str(None)][ + str(attrib) + ].append(value) else: if str(None) in main_responsive_dict[str(None)]: - main_responsive_dict[str(None)][str(None)].append(value) + main_responsive_dict[str(None)][str(None)].append( + value + ) else: main_responsive_dict[str(None)][str(None)] = [] - main_responsive_dict[str(None)][str(None)].append(value) + main_responsive_dict[str(None)][str(None)].append( + value + ) else: main_group_attribute = resolve_keys(sub_group_by, value) group_attribute = resolve_keys(group_by, value) @@ -118,13 +185,22 @@ def group_results(results_data, group_by, sub_group_by=False): if str(main_group_attribute) not in main_responsive_dict: main_responsive_dict[str(main_group_attribute)] = {} - if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]: - main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value) + if ( + str(group_attribute) + in main_responsive_dict[str(main_group_attribute)] + ): + main_responsive_dict[str(main_group_attribute)][ + str(group_attribute) + ].append(value) else: - main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = [] - main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value) + main_responsive_dict[str(main_group_attribute)][ + str(group_attribute) + ] = [] + main_responsive_dict[str(main_group_attribute)][ + str(group_attribute) + ].append(value) - return main_responsive_dict + return main_responsive_dict else: response_dict = {} diff --git a/apiserver/plane/utils/html_processor.py b/apiserver/plane/utils/html_processor.py index 5f61607e9..18d103b64 100644 --- a/apiserver/plane/utils/html_processor.py +++ b/apiserver/plane/utils/html_processor.py @@ -1,15 +1,17 @@ from io import StringIO from html.parser import HTMLParser + class MLStripper(HTMLParser): """ Markup Language Stripper """ + def __init__(self): super().__init__() self.reset() self.strict = False - self.convert_charrefs= True + self.convert_charrefs = True self.text = StringIO() def handle_data(self, d): @@ -18,6 +20,7 @@ class MLStripper(HTMLParser): def get_data(self): return self.text.getvalue() + def strip_tags(html): s = MLStripper() s.feed(html) diff --git a/apiserver/plane/utils/importers/jira.py b/apiserver/plane/utils/importers/jira.py index b427ba14f..6f3a7c217 100644 --- a/apiserver/plane/utils/importers/jira.py +++ b/apiserver/plane/utils/importers/jira.py @@ -1,35 +1,97 @@ import requests +import re from requests.auth import HTTPBasicAuth from sentry_sdk import capture_exception +from urllib.parse import urlparse, urljoin + + +def is_allowed_hostname(hostname): + allowed_domains = [ + "atl-paas.net", + "atlassian.com", + "atlassian.net", + "jira.com", + ] + parsed_uri = urlparse(f"https://{hostname}") + domain = parsed_uri.netloc.split(":")[0] # Ensures no port is included + base_domain = ".".join(domain.split(".")[-2:]) + return base_domain in allowed_domains + + +def is_valid_project_key(project_key): + if project_key: + project_key = project_key.strip().upper() + # Adjust the regular expression as needed based on your specific requirements. + if len(project_key) > 30: + return False + # Check the validity of the key as well + pattern = re.compile(r"^[A-Z0-9]{1,10}$") + return pattern.match(project_key) is not None + else: + False + + +def generate_valid_project_key(project_key): + return project_key.strip().upper() + + +def generate_url(hostname, path): + if not is_allowed_hostname(hostname): + raise ValueError("Invalid or unauthorized hostname") + return urljoin(f"https://{hostname}", path) def jira_project_issue_summary(email, api_token, project_key, hostname): try: + if not is_allowed_hostname(hostname): + return {"error": "Invalid or unauthorized hostname"} + + if not is_valid_project_key(project_key): + return {"error": "Invalid project key"} + auth = HTTPBasicAuth(email, api_token) headers = {"Accept": "application/json"} - issue_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype=Story" + # make the project key upper case + project_key = generate_valid_project_key(project_key) + + # issues + issue_url = generate_url( + hostname, + f"/rest/api/3/search?jql=project={project_key} AND issuetype!=Epic", + ) issue_response = requests.request( "GET", issue_url, headers=headers, auth=auth ).json()["total"] - module_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype=Epic" + # modules + module_url = generate_url( + hostname, + f"/rest/api/3/search?jql=project={project_key} AND issuetype=Epic", + ) module_response = requests.request( "GET", module_url, headers=headers, auth=auth ).json()["total"] - status_url = f"https://{hostname}/rest/api/3/status/?jql=project={project_key}" + # status + status_url = generate_url( + hostname, f"/rest/api/3/project/${project_key}/statuses" + ) status_response = requests.request( "GET", status_url, headers=headers, auth=auth ).json() - labels_url = f"https://{hostname}/rest/api/3/label/?jql=project={project_key}" + # labels + labels_url = generate_url( + hostname, f"/rest/api/3/label/?jql=project={project_key}" + ) labels_response = requests.request( "GET", labels_url, headers=headers, auth=auth ).json()["total"] - users_url = ( - f"https://{hostname}/rest/api/3/users/search?jql=project={project_key}" + # users + users_url = generate_url( + hostname, f"/rest/api/3/users/search?jql=project={project_key}" ) users_response = requests.request( "GET", users_url, headers=headers, auth=auth @@ -50,4 +112,6 @@ def jira_project_issue_summary(email, api_token, project_key, hostname): } except Exception as e: capture_exception(e) - return {"error": "Something went wrong could not fetch information from jira"} + return { + "error": "Something went wrong could not fetch information from jira" + } diff --git a/apiserver/plane/utils/imports.py b/apiserver/plane/utils/imports.py index 5f9f1c98c..89753ef1d 100644 --- a/apiserver/plane/utils/imports.py +++ b/apiserver/plane/utils/imports.py @@ -8,13 +8,12 @@ def import_submodules(context, root_module, path): >>> import_submodules(locals(), __name__, __path__) """ for loader, module_name, is_pkg in pkgutil.walk_packages( - path, - root_module + - '.'): + path, root_module + "." + ): # this causes a Runtime error with model conflicts # module = loader.find_module(module_name).load_module(module_name) - module = __import__(module_name, globals(), locals(), ['__name__']) + module = __import__(module_name, globals(), locals(), ["__name__"]) for k, v in six.iteritems(vars(module)): - if not k.startswith('_'): + if not k.startswith("_"): context[k] = v context[module_name] = module diff --git a/apiserver/plane/utils/integrations/github.py b/apiserver/plane/utils/integrations/github.py index 45cb5925a..5a7ce2aa2 100644 --- a/apiserver/plane/utils/integrations/github.py +++ b/apiserver/plane/utils/integrations/github.py @@ -10,7 +10,9 @@ from django.conf import settings def get_jwt_token(): app_id = os.environ.get("GITHUB_APP_ID", "") - secret = bytes(os.environ.get("GITHUB_APP_PRIVATE_KEY", ""), encoding="utf8") + secret = bytes( + os.environ.get("GITHUB_APP_PRIVATE_KEY", ""), encoding="utf8" + ) current_timestamp = int(datetime.now().timestamp()) due_date = datetime.now() + timedelta(minutes=10) expiry = int(due_date.timestamp()) diff --git a/apiserver/plane/utils/integrations/slack.py b/apiserver/plane/utils/integrations/slack.py index 70f26e160..0cc5b93b2 100644 --- a/apiserver/plane/utils/integrations/slack.py +++ b/apiserver/plane/utils/integrations/slack.py @@ -1,6 +1,7 @@ import os import requests + def slack_oauth(code): SLACK_OAUTH_URL = os.environ.get("SLACK_OAUTH_URL", False) SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID", False) diff --git a/apiserver/plane/utils/ip_address.py b/apiserver/plane/utils/ip_address.py index 06ca4353d..01789c431 100644 --- a/apiserver/plane/utils/ip_address.py +++ b/apiserver/plane/utils/ip_address.py @@ -1,7 +1,7 @@ def get_client_ip(request): - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] + ip = x_forwarded_for.split(",")[0] else: - ip = request.META.get('REMOTE_ADDR') + ip = request.META.get("REMOTE_ADDR") return ip diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 2da24092a..87284ff24 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -3,10 +3,10 @@ import uuid from datetime import timedelta from django.utils import timezone - # The date from pattern pattern = re.compile(r"\d+_(weeks|months)$") + # check the valid uuids def filter_valid_uuids(uuid_list): valid_uuids = [] @@ -21,19 +21,29 @@ def filter_valid_uuids(uuid_list): # Get the 2_weeks, 3_months -def string_date_filter(filter, duration, subsequent, term, date_filter, offset): +def string_date_filter( + filter, duration, subsequent, term, date_filter, offset +): now = timezone.now().date() if term == "months": if subsequent == "after": if offset == "fromnow": - filter[f"{date_filter}__gte"] = now + timedelta(days=duration * 30) + filter[f"{date_filter}__gte"] = now + timedelta( + days=duration * 30 + ) else: - filter[f"{date_filter}__gte"] = now - timedelta(days=duration * 30) + filter[f"{date_filter}__gte"] = now - timedelta( + days=duration * 30 + ) else: if offset == "fromnow": - filter[f"{date_filter}__lte"] = now + timedelta(days=duration * 30) + filter[f"{date_filter}__lte"] = now + timedelta( + days=duration * 30 + ) else: - filter[f"{date_filter}__lte"] = now - timedelta(days=duration * 30) + filter[f"{date_filter}__lte"] = now - timedelta( + days=duration * 30 + ) if term == "weeks": if subsequent == "after": if offset == "fromnow": @@ -49,7 +59,7 @@ def string_date_filter(filter, duration, subsequent, term, date_filter, offset): def date_filter(filter, date_term, queries): """ - Handle all date filters + Handle all date filters """ for query in queries: date_query = query.split(";") @@ -75,41 +85,67 @@ def date_filter(filter, date_term, queries): def filter_state(params, filter, method): if method == "GET": - states = [item for item in params.get("state").split(",") if item != 'null'] + states = [ + item for item in params.get("state").split(",") if item != "null" + ] states = filter_valid_uuids(states) if len(states) and "" not in states: filter["state__in"] = states else: - if params.get("state", None) and len(params.get("state")) and params.get("state") != 'null': + if ( + params.get("state", None) + and len(params.get("state")) + and params.get("state") != "null" + ): filter["state__in"] = params.get("state") return filter def filter_state_group(params, filter, method): if method == "GET": - state_group = [item for item in params.get("state_group").split(",") if item != 'null'] + state_group = [ + item + for item in params.get("state_group").split(",") + if item != "null" + ] if len(state_group) and "" not in state_group: filter["state__group__in"] = state_group else: - if params.get("state_group", None) and len(params.get("state_group")) and params.get("state_group") != 'null': + if ( + params.get("state_group", None) + and len(params.get("state_group")) + and params.get("state_group") != "null" + ): filter["state__group__in"] = params.get("state_group") return filter def filter_estimate_point(params, filter, method): if method == "GET": - estimate_points = [item for item in params.get("estimate_point").split(",") if item != 'null'] + estimate_points = [ + item + for item in params.get("estimate_point").split(",") + if item != "null" + ] if len(estimate_points) and "" not in estimate_points: filter["estimate_point__in"] = estimate_points else: - if params.get("estimate_point", None) and len(params.get("estimate_point")) and params.get("estimate_point") != 'null': + if ( + params.get("estimate_point", None) + and len(params.get("estimate_point")) + and params.get("estimate_point") != "null" + ): filter["estimate_point__in"] = params.get("estimate_point") return filter def filter_priority(params, filter, method): if method == "GET": - priorities = [item for item in params.get("priority").split(",") if item != 'null'] + priorities = [ + item + for item in params.get("priority").split(",") + if item != "null" + ] if len(priorities) and "" not in priorities: filter["priority__in"] = priorities return filter @@ -117,59 +153,96 @@ def filter_priority(params, filter, method): def filter_parent(params, filter, method): if method == "GET": - parents = [item for item in params.get("parent").split(",") if item != 'null'] + parents = [ + item for item in params.get("parent").split(",") if item != "null" + ] parents = filter_valid_uuids(parents) if len(parents) and "" not in parents: filter["parent__in"] = parents else: - if params.get("parent", None) and len(params.get("parent")) and params.get("parent") != 'null': + if ( + params.get("parent", None) + and len(params.get("parent")) + and params.get("parent") != "null" + ): filter["parent__in"] = params.get("parent") return filter def filter_labels(params, filter, method): if method == "GET": - labels = [item for item in params.get("labels").split(",") if item != 'null'] + labels = [ + item for item in params.get("labels").split(",") if item != "null" + ] labels = filter_valid_uuids(labels) if len(labels) and "" not in labels: filter["labels__in"] = labels else: - if params.get("labels", None) and len(params.get("labels")) and params.get("labels") != 'null': + if ( + params.get("labels", None) + and len(params.get("labels")) + and params.get("labels") != "null" + ): filter["labels__in"] = params.get("labels") return filter def filter_assignees(params, filter, method): if method == "GET": - assignees = [item for item in params.get("assignees").split(",") if item != 'null'] + assignees = [ + item + for item in params.get("assignees").split(",") + if item != "null" + ] assignees = filter_valid_uuids(assignees) if len(assignees) and "" not in assignees: filter["assignees__in"] = assignees else: - if params.get("assignees", None) and len(params.get("assignees")) and params.get("assignees") != 'null': + if ( + params.get("assignees", None) + and len(params.get("assignees")) + and params.get("assignees") != "null" + ): filter["assignees__in"] = params.get("assignees") return filter + def filter_mentions(params, filter, method): if method == "GET": - mentions = [item for item in params.get("mentions").split(",") if item != 'null'] + mentions = [ + item + for item in params.get("mentions").split(",") + if item != "null" + ] mentions = filter_valid_uuids(mentions) if len(mentions) and "" not in mentions: filter["issue_mention__mention__id__in"] = mentions else: - if params.get("mentions", None) and len(params.get("mentions")) and params.get("mentions") != 'null': + if ( + params.get("mentions", None) + and len(params.get("mentions")) + and params.get("mentions") != "null" + ): filter["issue_mention__mention__id__in"] = params.get("mentions") return filter def filter_created_by(params, filter, method): if method == "GET": - created_bys = [item for item in params.get("created_by").split(",") if item != 'null'] + created_bys = [ + item + for item in params.get("created_by").split(",") + if item != "null" + ] created_bys = filter_valid_uuids(created_bys) if len(created_bys) and "" not in created_bys: filter["created_by__in"] = created_bys else: - if params.get("created_by", None) and len(params.get("created_by")) and params.get("created_by") != 'null': + if ( + params.get("created_by", None) + and len(params.get("created_by")) + and params.get("created_by") != "null" + ): filter["created_by__in"] = params.get("created_by") return filter @@ -184,10 +257,18 @@ def filter_created_at(params, filter, method): if method == "GET": created_ats = params.get("created_at").split(",") if len(created_ats) and "" not in created_ats: - date_filter(filter=filter, date_term="created_at__date", queries=created_ats) + date_filter( + filter=filter, + date_term="created_at__date", + queries=created_ats, + ) else: if params.get("created_at", None) and len(params.get("created_at")): - date_filter(filter=filter, date_term="created_at__date", queries=params.get("created_at", [])) + date_filter( + filter=filter, + date_term="created_at__date", + queries=params.get("created_at", []), + ) return filter @@ -195,10 +276,18 @@ def filter_updated_at(params, filter, method): if method == "GET": updated_ats = params.get("updated_at").split(",") if len(updated_ats) and "" not in updated_ats: - date_filter(filter=filter, date_term="created_at__date", queries=updated_ats) + date_filter( + filter=filter, + date_term="created_at__date", + queries=updated_ats, + ) else: if params.get("updated_at", None) and len(params.get("updated_at")): - date_filter(filter=filter, date_term="created_at__date", queries=params.get("updated_at", [])) + date_filter( + filter=filter, + date_term="created_at__date", + queries=params.get("updated_at", []), + ) return filter @@ -206,7 +295,9 @@ def filter_start_date(params, filter, method): if method == "GET": start_dates = params.get("start_date").split(",") if len(start_dates) and "" not in start_dates: - date_filter(filter=filter, date_term="start_date", queries=start_dates) + date_filter( + filter=filter, date_term="start_date", queries=start_dates + ) else: if params.get("start_date", None) and len(params.get("start_date")): filter["start_date"] = params.get("start_date") @@ -217,7 +308,9 @@ def filter_target_date(params, filter, method): if method == "GET": target_dates = params.get("target_date").split(",") if len(target_dates) and "" not in target_dates: - date_filter(filter=filter, date_term="target_date", queries=target_dates) + date_filter( + filter=filter, date_term="target_date", queries=target_dates + ) else: if params.get("target_date", None) and len(params.get("target_date")): filter["target_date"] = params.get("target_date") @@ -228,10 +321,20 @@ def filter_completed_at(params, filter, method): if method == "GET": completed_ats = params.get("completed_at").split(",") if len(completed_ats) and "" not in completed_ats: - date_filter(filter=filter, date_term="completed_at__date", queries=completed_ats) + date_filter( + filter=filter, + date_term="completed_at__date", + queries=completed_ats, + ) else: - if params.get("completed_at", None) and len(params.get("completed_at")): - date_filter(filter=filter, date_term="completed_at__date", queries=params.get("completed_at", [])) + if params.get("completed_at", None) and len( + params.get("completed_at") + ): + date_filter( + filter=filter, + date_term="completed_at__date", + queries=params.get("completed_at", []), + ) return filter @@ -249,47 +352,73 @@ def filter_issue_state_type(params, filter, method): def filter_project(params, filter, method): if method == "GET": - projects = [item for item in params.get("project").split(",") if item != 'null'] + projects = [ + item for item in params.get("project").split(",") if item != "null" + ] projects = filter_valid_uuids(projects) if len(projects) and "" not in projects: filter["project__in"] = projects else: - if params.get("project", None) and len(params.get("project")) and params.get("project") != 'null': + if ( + params.get("project", None) + and len(params.get("project")) + and params.get("project") != "null" + ): filter["project__in"] = params.get("project") return filter def filter_cycle(params, filter, method): if method == "GET": - cycles = [item for item in params.get("cycle").split(",") if item != 'null'] + cycles = [ + item for item in params.get("cycle").split(",") if item != "null" + ] cycles = filter_valid_uuids(cycles) if len(cycles) and "" not in cycles: filter["issue_cycle__cycle_id__in"] = cycles else: - if params.get("cycle", None) and len(params.get("cycle")) and params.get("cycle") != 'null': + if ( + params.get("cycle", None) + and len(params.get("cycle")) + and params.get("cycle") != "null" + ): filter["issue_cycle__cycle_id__in"] = params.get("cycle") return filter def filter_module(params, filter, method): if method == "GET": - modules = [item for item in params.get("module").split(",") if item != 'null'] + modules = [ + item for item in params.get("module").split(",") if item != "null" + ] modules = filter_valid_uuids(modules) if len(modules) and "" not in modules: filter["issue_module__module_id__in"] = modules else: - if params.get("module", None) and len(params.get("module")) and params.get("module") != 'null': + if ( + params.get("module", None) + and len(params.get("module")) + and params.get("module") != "null" + ): filter["issue_module__module_id__in"] = params.get("module") return filter def filter_inbox_status(params, filter, method): if method == "GET": - status = [item for item in params.get("inbox_status").split(",") if item != 'null'] + status = [ + item + for item in params.get("inbox_status").split(",") + if item != "null" + ] if len(status) and "" not in status: filter["issue_inbox__status__in"] = status else: - if params.get("inbox_status", None) and len(params.get("inbox_status")) and params.get("inbox_status") != 'null': + if ( + params.get("inbox_status", None) + and len(params.get("inbox_status")) + and params.get("inbox_status") != "null" + ): filter["issue_inbox__status__in"] = params.get("inbox_status") return filter @@ -308,13 +437,23 @@ def filter_sub_issue_toggle(params, filter, method): def filter_subscribed_issues(params, filter, method): if method == "GET": - subscribers = [item for item in params.get("subscriber").split(",") if item != 'null'] + subscribers = [ + item + for item in params.get("subscriber").split(",") + if item != "null" + ] subscribers = filter_valid_uuids(subscribers) if len(subscribers) and "" not in subscribers: filter["issue_subscribers__subscriber_id__in"] = subscribers else: - if params.get("subscriber", None) and len(params.get("subscriber")) and params.get("subscriber") != 'null': - filter["issue_subscribers__subscriber_id__in"] = params.get("subscriber") + if ( + params.get("subscriber", None) + and len(params.get("subscriber")) + and params.get("subscriber") != "null" + ): + filter["issue_subscribers__subscriber_id__in"] = params.get( + "subscriber" + ) return filter @@ -324,7 +463,7 @@ def filter_start_target_date_issues(params, filter, method): filter["target_date__isnull"] = False filter["start_date__isnull"] = False return filter - + def issue_filters(query_params, method): filter = {} diff --git a/apiserver/plane/utils/issue_search.py b/apiserver/plane/utils/issue_search.py index 40f85dde4..d38b1f4c3 100644 --- a/apiserver/plane/utils/issue_search.py +++ b/apiserver/plane/utils/issue_search.py @@ -12,8 +12,8 @@ def search_issues(query, queryset): fields = ["name", "sequence_id"] q = Q() for field in fields: - if field == "sequence_id": - sequences = re.findall(r"\d+\.\d+|\d+", query) + if field == "sequence_id" and len(query) <= 20: + sequences = re.findall(r"[A-Za-z0-9]{1,12}-\d+", query) for sequence_id in sequences: q |= Q(**{"sequence_id": sequence_id}) else: diff --git a/apiserver/plane/utils/paginator.py b/apiserver/plane/utils/paginator.py index 793614cc0..6b2b49c15 100644 --- a/apiserver/plane/utils/paginator.py +++ b/apiserver/plane/utils/paginator.py @@ -31,8 +31,10 @@ class Cursor: try: bits = value.split(":") if len(bits) != 3: - raise ValueError("Cursor must be in the format 'value:offset:is_prev'") - + raise ValueError( + "Cursor must be in the format 'value:offset:is_prev'" + ) + value = float(bits[0]) if "." in bits[0] else int(bits[0]) return cls(value, int(bits[1]), bool(int(bits[2]))) except (TypeError, ValueError) as e: @@ -178,7 +180,9 @@ class BasePaginator: input_cursor = None if request.GET.get(self.cursor_name): try: - input_cursor = cursor_cls.from_string(request.GET.get(self.cursor_name)) + input_cursor = cursor_cls.from_string( + request.GET.get(self.cursor_name) + ) except ValueError: raise ParseError(detail="Invalid cursor parameter.") @@ -186,9 +190,11 @@ class BasePaginator: paginator = paginator_cls(**paginator_kwargs) try: - cursor_result = paginator.get_result(limit=per_page, cursor=input_cursor) + cursor_result = paginator.get_result( + limit=per_page, cursor=input_cursor + ) except BadPaginationError as e: - raise ParseError(detail=str(e)) + raise ParseError(detail="Error in parsing") # Serialize result according to the on_result function if on_results: diff --git a/apiserver/plane/web/apps.py b/apiserver/plane/web/apps.py index 76ca3c4e6..a5861f9b5 100644 --- a/apiserver/plane/web/apps.py +++ b/apiserver/plane/web/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class WebConfig(AppConfig): - name = 'plane.web' + name = "plane.web" diff --git a/apiserver/plane/web/urls.py b/apiserver/plane/web/urls.py index 568b99037..24a3e7b57 100644 --- a/apiserver/plane/web/urls.py +++ b/apiserver/plane/web/urls.py @@ -2,6 +2,5 @@ from django.urls import path from django.views.generic import TemplateView urlpatterns = [ - path('about/', TemplateView.as_view(template_name='about.html')) - + path("about/", TemplateView.as_view(template_name="about.html")) ] diff --git a/apiserver/plane/wsgi.py b/apiserver/plane/wsgi.py index ef3ea2780..b3051f9ff 100644 --- a/apiserver/plane/wsgi.py +++ b/apiserver/plane/wsgi.py @@ -9,7 +9,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', - 'plane.settings.production') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") application = get_wsgi_application() diff --git a/apiserver/pyproject.toml b/apiserver/pyproject.toml new file mode 100644 index 000000000..773d6090e --- /dev/null +++ b/apiserver/pyproject.toml @@ -0,0 +1,18 @@ +[tool.black] +line-length = 79 +target-version = ['py36'] +include = '\.pyi?$' +exclude = ''' + /( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | venv + )/ +''' diff --git a/deploy/selfhost/build.yml b/deploy/selfhost/build.yml new file mode 100644 index 000000000..92533a73b --- /dev/null +++ b/deploy/selfhost/build.yml @@ -0,0 +1,26 @@ +version: "3.8" + +services: + web: + image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest} + build: + context: . + dockerfile: ./web/Dockerfile.web + + space: + image: ${DOCKERHUB_USER:-local}/plane-space:${APP_RELEASE:-latest} + build: + context: ./ + dockerfile: ./space/Dockerfile.space + + api: + image: ${DOCKERHUB_USER:-local}/plane-backend:${APP_RELEASE:-latest} + build: + context: ./apiserver + dockerfile: ./Dockerfile.api + + proxy: + image: ${DOCKERHUB_USER:-local}/plane-proxy:${APP_RELEASE:-latest} + build: + context: ./nginx + dockerfile: ./Dockerfile diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index 8b4ff77ef..e42f53c7a 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -65,8 +65,8 @@ x-app-env : &app-env services: web: <<: *app-env - platform: linux/amd64 - image: makeplane/plane-frontend:${APP_RELEASE:-latest} + image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-latest} + pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: /usr/local/bin/start.sh web/server.js web deploy: @@ -77,8 +77,8 @@ services: space: <<: *app-env - platform: linux/amd64 - image: makeplane/plane-space:${APP_RELEASE:-latest} + image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-latest} + pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: /usr/local/bin/start.sh space/server.js space deploy: @@ -90,8 +90,8 @@ services: api: <<: *app-env - platform: linux/amd64 - image: makeplane/plane-backend:${APP_RELEASE:-latest} + image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest} + pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: ./bin/takeoff deploy: @@ -102,8 +102,8 @@ services: worker: <<: *app-env - platform: linux/amd64 - image: makeplane/plane-backend:${APP_RELEASE:-latest} + image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest} + pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: ./bin/worker depends_on: @@ -113,8 +113,8 @@ services: beat-worker: <<: *app-env - platform: linux/amd64 - image: makeplane/plane-backend:${APP_RELEASE:-latest} + image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest} + pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: ./bin/beat depends_on: @@ -125,6 +125,7 @@ services: plane-db: <<: *app-env image: postgres:15.2-alpine + pull_policy: if_not_present restart: unless-stopped command: postgres -c 'max_connections=1000' volumes: @@ -133,6 +134,7 @@ services: plane-redis: <<: *app-env image: redis:6.2.7-alpine + pull_policy: if_not_present restart: unless-stopped volumes: - redisdata:/data @@ -140,6 +142,7 @@ services: plane-minio: <<: *app-env image: minio/minio + pull_policy: if_not_present restart: unless-stopped command: server /export --console-address ":9090" volumes: @@ -148,8 +151,8 @@ services: # Comment this if you already have a reverse proxy running proxy: <<: *app-env - platform: linux/amd64 - image: makeplane/plane-proxy:${APP_RELEASE:-latest} + image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-latest} + pull_policy: ${PULL_POLICY:-always} ports: - ${NGINX_PORT}:80 depends_on: diff --git a/deploy/selfhost/install.sh b/deploy/selfhost/install.sh index 645e99cb8..3f306c559 100755 --- a/deploy/selfhost/install.sh +++ b/deploy/selfhost/install.sh @@ -3,13 +3,75 @@ BRANCH=master SCRIPT_DIR=$PWD PLANE_INSTALL_DIR=$PWD/plane-app +export APP_RELEASE=$BRANCH +export DOCKERHUB_USER=makeplane +export PULL_POLICY=always +USE_GLOBAL_IMAGES=1 -function install(){ - echo - echo "Installing on $PLANE_INSTALL_DIR" +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +function buildLocalImage() { + if [ "$1" == "--force-build" ]; then + DO_BUILD="1" + elif [ "$1" == "--skip-build" ]; then + DO_BUILD="2" + else + printf "\n" >&2 + printf "${YELLOW}You are on ${ARCH} cpu architecture. ${NC}\n" >&2 + printf "${YELLOW}Since the prebuilt ${ARCH} compatible docker images are not available for, we will be running the docker build on this system. ${NC} \n" >&2 + printf "${YELLOW}This might take ${YELLOW}5-30 min based on your system's hardware configuration. \n ${NC} \n" >&2 + printf "\n" >&2 + printf "${GREEN}Select an option to proceed: ${NC}\n" >&2 + printf " 1) Build Fresh Images \n" >&2 + printf " 2) Skip Building Images \n" >&2 + printf " 3) Exit \n" >&2 + printf "\n" >&2 + read -p "Select Option [1]: " DO_BUILD + until [[ -z "$DO_BUILD" || "$DO_BUILD" =~ ^[1-3]$ ]]; do + echo "$DO_BUILD: invalid selection." >&2 + read -p "Select Option [1]: " DO_BUILD + done + echo "" >&2 + fi + + if [ "$DO_BUILD" == "1" ] || [ "$DO_BUILD" == "" ]; + then + REPO=https://github.com/makeplane/plane.git + CURR_DIR=$PWD + PLANE_TEMP_CODE_DIR=$(mktemp -d) + git clone $REPO $PLANE_TEMP_CODE_DIR --branch $BRANCH --single-branch + + cp $PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml $PLANE_TEMP_CODE_DIR/build.yml + + cd $PLANE_TEMP_CODE_DIR + if [ "$BRANCH" == "master" ]; + then + APP_RELEASE=latest + fi + + docker compose -f build.yml build --no-cache >&2 + # cd $CURR_DIR + # rm -rf $PLANE_TEMP_CODE_DIR + echo "build_completed" + elif [ "$DO_BUILD" == "2" ]; + then + printf "${YELLOW}Build action skipped by you in lieu of using existing images. ${NC} \n" >&2 + echo "build_skipped" + elif [ "$DO_BUILD" == "3" ]; + then + echo "build_exited" + else + printf "INVALID OPTION SUPPLIED" >&2 + fi +} +function install() { + echo "Installing Plane.........." download } -function download(){ +function download() { cd $SCRIPT_DIR TS=$(date +%s) if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ] @@ -35,6 +97,21 @@ function download(){ rm $PLANE_INSTALL_DIR/temp.yaml fi + + if [ $USE_GLOBAL_IMAGES == 0 ]; then + local res=$(buildLocalImage) + # echo $res + + if [ "$res" == "build_exited" ]; + then + echo + echo "Install action cancelled by you. Exiting now." + echo + exit 0 + fi + else + docker compose -f $PLANE_INSTALL_DIR/docker-compose.yaml pull + fi echo "" echo "Latest version is now available for you to use" @@ -43,22 +120,22 @@ function download(){ echo "" } -function startServices(){ +function startServices() { cd $PLANE_INSTALL_DIR - docker compose up -d + docker compose up -d --quiet-pull cd $SCRIPT_DIR } -function stopServices(){ +function stopServices() { cd $PLANE_INSTALL_DIR docker compose down cd $SCRIPT_DIR } -function restartServices(){ +function restartServices() { cd $PLANE_INSTALL_DIR docker compose restart cd $SCRIPT_DIR } -function upgrade(){ +function upgrade() { echo "***** STOPPING SERVICES ****" stopServices @@ -69,10 +146,10 @@ function upgrade(){ echo "***** PLEASE VALIDATE AND START SERVICES ****" } -function askForAction(){ +function askForAction() { echo echo "Select a Action you want to perform:" - echo " 1) Install" + echo " 1) Install (${ARCH})" echo " 2) Start" echo " 3) Stop" echo " 4) Restart" @@ -115,6 +192,20 @@ function askForAction(){ fi } +# CPU ARCHITECHTURE BASED SETTINGS +ARCH=$(uname -m) +if [ $ARCH == "amd64" ] || [ $ARCH == "x86_64" ]; +then + USE_GLOBAL_IMAGES=1 + DOCKERHUB_USER=makeplane + PULL_POLICY=always +else + USE_GLOBAL_IMAGES=0 + DOCKERHUB_USER=myplane + PULL_POLICY=never +fi + +# REMOVE SPECIAL CHARACTERS FROM BRANCH NAME if [ "$BRANCH" != "master" ]; then PLANE_INSTALL_DIR=$PWD/plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g') diff --git a/docker-compose-local.yml b/docker-compose-local.yml index 4e1e3b39f..b0fb9da24 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -44,9 +44,6 @@ services: env_file: - .env environment: - POSTGRES_USER: ${PGUSER} - POSTGRES_DB: ${PGDATABASE} - POSTGRES_PASSWORD: ${PGPASSWORD} PGDATA: /var/lib/postgresql/data web: diff --git a/nginx/env.sh b/nginx/env.sh index 59e4a46a0..7db471eca 100644 --- a/nginx/env.sh +++ b/nginx/env.sh @@ -1,4 +1,6 @@ #!/bin/sh +export dollar="$" +export http_upgrade="http_upgrade" envsubst < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf exec nginx -g 'daemon off;' diff --git a/nginx/nginx.conf.dev b/nginx/nginx.conf.dev index 182fc4d83..f86c84aa8 100644 --- a/nginx/nginx.conf.dev +++ b/nginx/nginx.conf.dev @@ -19,7 +19,7 @@ http { location / { proxy_pass http://web:3000/; proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; + proxy_set_header Upgrade ${dollar}http_upgrade; proxy_set_header Connection "upgrade"; } diff --git a/package.json b/package.json index aad104784..a964c69fa 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "prettier": "latest", "prettier-plugin-tailwindcss": "^0.5.4", "tailwindcss": "^3.3.3", - "turbo": "^1.11.2" + "turbo": "^1.11.3" }, "resolutions": { "@types/react": "18.2.42" diff --git a/packages/editor/core/src/ui/components/editor-container.tsx b/packages/editor/core/src/ui/components/editor-container.tsx index 8de6298b5..4be5c9843 100644 --- a/packages/editor/core/src/ui/components/editor-container.tsx +++ b/packages/editor/core/src/ui/components/editor-container.tsx @@ -5,14 +5,18 @@ interface EditorContainerProps { editor: Editor | null; editorClassNames: string; children: ReactNode; + hideDragHandle?: () => void; } -export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => ( +export const EditorContainer = ({ editor, editorClassNames, hideDragHandle, children }: EditorContainerProps) => (
{ editor?.chain().focus().run(); }} + onMouseLeave={() => { + hideDragHandle?.(); + }} className={`cursor-text ${editorClassNames}`} > {children} diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index fab0d5b74..19d8ce894 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -56,6 +56,7 @@ export const CoreEditorExtensions = ( code: false, codeBlock: false, horizontalRule: false, + blockquote: false, dropcursor: { color: "rgba(var(--color-text-100))", width: 2, diff --git a/packages/editor/core/src/ui/extensions/quote/index.tsx b/packages/editor/core/src/ui/extensions/quote/index.tsx index de8e85eca..8fed1ced2 100644 --- a/packages/editor/core/src/ui/extensions/quote/index.tsx +++ b/packages/editor/core/src/ui/extensions/quote/index.tsx @@ -1,10 +1,9 @@ -import { isAtStartOfNode } from "@tiptap/core"; import Blockquote from "@tiptap/extension-blockquote"; export const CustomQuoteExtension = Blockquote.extend({ addKeyboardShortcuts() { return { - Enter: ({ editor }) => { + Enter: () => { const { $from, $to, $head } = this.editor.state.selection; const parent = $head.node(-1); diff --git a/packages/editor/document-editor/package.json b/packages/editor/document-editor/package.json index 72dfab954..21d610751 100644 --- a/packages/editor/document-editor/package.json +++ b/packages/editor/document-editor/package.json @@ -28,6 +28,7 @@ "react-dom": "18.2.0" }, "dependencies": { + "@floating-ui/react": "^0.26.4", "@plane/editor-core": "*", "@plane/editor-extensions": "*", "@plane/ui": "*", @@ -36,6 +37,7 @@ "@tiptap/pm": "^2.1.13", "@tiptap/suggestion": "^2.1.13", "eslint-config-next": "13.2.4", + "lucide-react": "^0.309.0", "react-popper": "^2.3.0", "tippy.js": "^6.3.7", "uuid": "^9.0.1" diff --git a/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx b/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx new file mode 100644 index 000000000..136d04e01 --- /dev/null +++ b/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx @@ -0,0 +1,148 @@ +import { isValidHttpUrl } from "@plane/editor-core"; +import { Node } from "@tiptap/pm/model"; +import { Link2Off } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { LinkViewProps } from "./link-view"; + +const InputView = ({ + label, + defaultValue, + placeholder, + onChange, +}: { + label: string; + defaultValue: string; + placeholder: string; + onChange: (e: React.ChangeEvent) => void; +}) => ( +
+ + { + e.stopPropagation(); + }} + className="w-[280px] outline-none bg-custom-background-90 text-custom-text-900 text-sm" + defaultValue={defaultValue} + onChange={onChange} + /> +
+); + +export const LinkEditView = ({ + viewProps, +}: { + viewProps: LinkViewProps; + switchView: (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => void; +}) => { + const { editor, from, to } = viewProps; + + const [positionRef, setPositionRef] = useState({ from: from, to: to }); + const [localUrl, setLocalUrl] = useState(viewProps.url); + + const linkRemoved = useRef(); + + const getText = (from: number, to: number) => { + const text = editor.state.doc.textBetween(from, to, "\n"); + return text; + }; + + const isValidUrl = (urlString: string) => { + var urlPattern = new RegExp( + "^(https?:\\/\\/)?" + // validate protocol + "([\\w-]+\\.)+[\\w-]{2,}" + // validate domain name + "|((\\d{1,3}\\.){3}\\d{1,3})" + // validate IP (v4) address + "(\\:\\d+)?(\\/[-\\w.%]+)*" + // validate port and path + "(\\?[;&\\w.%=-]*)?" + // validate query string + "(\\#[-\\w]*)?$", // validate fragment locator + "i" + ); + const regexTest = urlPattern.test(urlString); + const urlTest = isValidHttpUrl(urlString); // Ensure you have defined isValidHttpUrl + return regexTest && urlTest; + }; + + const handleUpdateLink = (url: string) => { + setLocalUrl(url); + }; + + useEffect( + () => () => { + if (linkRemoved.current) return; + + const url = isValidUrl(localUrl) ? localUrl : viewProps.url; + + editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link)); + editor.view.dispatch(editor.state.tr.addMark(from, to, editor.schema.marks.link.create({ href: url }))); + }, + [localUrl] + ); + + const handleUpdateText = (text: string) => { + if (text === "") { + return; + } + + const node = editor.view.state.doc.nodeAt(from) as Node; + if (!node) return; + const marks = node.marks; + if (!marks) return; + + editor.chain().setTextSelection(from).run(); + + editor.chain().deleteRange({ from: positionRef.from, to: positionRef.to }).run(); + editor.chain().insertContent(text).run(); + + editor + .chain() + .setTextSelection({ + from: from, + to: from + text.length, + }) + .run(); + + setPositionRef({ from: from, to: from + text.length }); + + marks.forEach((mark) => { + editor.chain().setMark(mark.type.name, mark.attrs).run(); + }); + }; + + const removeLink = () => { + editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link)); + linkRemoved.current = true; + viewProps.onActionCompleteHandler({ + title: "Link successfully removed", + message: "The link was removed from the text.", + type: "success", + }); + viewProps.closeLinkView(); + }; + + return ( +
e.key === "Enter" && viewProps.closeLinkView()} + className="shadow-md rounded p-2 flex flex-col gap-3 bg-custom-background-90 border-custom-border-100 border-2" + > + handleUpdateLink(e.target.value)} + /> + handleUpdateText(e.target.value)} + /> +
+
+ + +
+
+ ); +}; diff --git a/packages/editor/document-editor/src/ui/components/links/link-input-view.tsx b/packages/editor/document-editor/src/ui/components/links/link-input-view.tsx new file mode 100644 index 000000000..fa73adbe1 --- /dev/null +++ b/packages/editor/document-editor/src/ui/components/links/link-input-view.tsx @@ -0,0 +1,9 @@ +import { LinkViewProps } from "./link-view"; + +export const LinkInputView = ({ + viewProps, + switchView, +}: { + viewProps: LinkViewProps; + switchView: (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => void; +}) =>

LinkInputView

; diff --git a/packages/editor/document-editor/src/ui/components/links/link-preview.tsx b/packages/editor/document-editor/src/ui/components/links/link-preview.tsx new file mode 100644 index 000000000..ff3fd0263 --- /dev/null +++ b/packages/editor/document-editor/src/ui/components/links/link-preview.tsx @@ -0,0 +1,52 @@ +import { Copy, GlobeIcon, Link2Off, PencilIcon } from "lucide-react"; +import { LinkViewProps } from "./link-view"; + +export const LinkPreview = ({ + viewProps, + switchView, +}: { + viewProps: LinkViewProps; + switchView: (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => void; +}) => { + const { editor, from, to, url } = viewProps; + + const removeLink = () => { + editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link)); + viewProps.onActionCompleteHandler({ + title: "Link successfully removed", + message: "The link was removed from the text.", + type: "success", + }); + viewProps.closeLinkView(); + }; + + const copyLinkToClipboard = () => { + navigator.clipboard.writeText(url); + viewProps.onActionCompleteHandler({ + title: "Link successfully copied", + message: "The link was copied to the clipboard.", + type: "success", + }); + viewProps.closeLinkView(); + }; + + return ( +
+
+ +

{url.length > 40 ? url.slice(0, 40) + "..." : url}

+
+ + + +
+
+
+ ); +}; diff --git a/packages/editor/document-editor/src/ui/components/links/link-view.tsx b/packages/editor/document-editor/src/ui/components/links/link-view.tsx new file mode 100644 index 000000000..f1d22a68e --- /dev/null +++ b/packages/editor/document-editor/src/ui/components/links/link-view.tsx @@ -0,0 +1,48 @@ +import { Editor } from "@tiptap/react"; +import { CSSProperties, useEffect, useState } from "react"; +import { LinkEditView } from "./link-edit-view"; +import { LinkInputView } from "./link-input-view"; +import { LinkPreview } from "./link-preview"; + +export interface LinkViewProps { + view?: "LinkPreview" | "LinkEditView" | "LinkInputView"; + editor: Editor; + from: number; + to: number; + url: string; + closeLinkView: () => void; + onActionCompleteHandler: (action: { + title: string; + message: string; + type: "success" | "error" | "warning" | "info"; + }) => void; +} + +export const LinkView = (props: LinkViewProps & { style: CSSProperties }) => { + const [currentView, setCurrentView] = useState(props.view ?? "LinkInputView"); + const [prevFrom, setPrevFrom] = useState(props.from); + + const switchView = (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => { + setCurrentView(view); + }; + + useEffect(() => { + if (props.from !== prevFrom) { + setCurrentView("LinkPreview"); + setPrevFrom(props.from); + } + }, []); + + const renderView = () => { + switch (currentView) { + case "LinkPreview": + return ; + case "LinkEditView": + return ; + case "LinkInputView": + return ; + } + }; + + return renderView(); +}; diff --git a/packages/editor/document-editor/src/ui/components/page-renderer.tsx b/packages/editor/document-editor/src/ui/components/page-renderer.tsx index c2d001abe..1bda353b8 100644 --- a/packages/editor/document-editor/src/ui/components/page-renderer.tsx +++ b/packages/editor/document-editor/src/ui/components/page-renderer.tsx @@ -1,12 +1,30 @@ import { EditorContainer, EditorContentWrapper } from "@plane/editor-core"; -import { Editor } from "@tiptap/react"; -import { useState } from "react"; +import { Node } from "@tiptap/pm/model"; +import { EditorView } from "@tiptap/pm/view"; +import { Editor, ReactRenderer } from "@tiptap/react"; +import { useCallback, useRef, useState } from "react"; import { DocumentDetails } from "src/types/editor-types"; +import { LinkView, LinkViewProps } from "./links/link-view"; +import { + autoUpdate, + computePosition, + flip, + hide, + shift, + useDismiss, + useFloating, + useInteractions, +} from "@floating-ui/react"; type IPageRenderer = { documentDetails: DocumentDetails; updatePageTitle: (title: string) => Promise; editor: Editor; + onActionCompleteHandler: (action: { + title: string; + message: string; + type: "success" | "error" | "warning" | "info"; + }) => void; editorClassNames: string; editorContentCustomClassNames?: string; readonly: boolean; @@ -29,6 +47,23 @@ export const PageRenderer = (props: IPageRenderer) => { const [pageTitle, setPagetitle] = useState(documentDetails.title); + const [linkViewProps, setLinkViewProps] = useState(); + const [isOpen, setIsOpen] = useState(false); + const [coordinates, setCoordinates] = useState<{ x: number; y: number }>(); + + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + middleware: [flip(), shift(), hide({ strategy: "referenceHidden" })], + whileElementsMounted: autoUpdate, + }); + + const dismiss = useDismiss(context, { + ancestorScroll: true, + }); + + const { getFloatingProps } = useInteractions([dismiss]); + const debouncedUpdatePageTitle = debounce(updatePageTitle, 300); const handlePageTitleChange = (title: string) => { @@ -36,8 +71,101 @@ export const PageRenderer = (props: IPageRenderer) => { debouncedUpdatePageTitle(title); }; + const [cleanup, setcleanup] = useState(() => () => {}); + + const floatingElementRef = useRef(null); + + const closeLinkView = () => { + setIsOpen(false); + }; + + const switchLinkView = (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => { + if (!linkViewProps) return; + setLinkViewProps({ + ...linkViewProps, + view: view, + }); + }; + + const handleLinkHover = useCallback( + (event: React.MouseEvent) => { + if (!editor) return; + const target = event.target as HTMLElement; + const view = editor.view as EditorView; + + if (!target || !view) return; + const pos = view.posAtDOM(target, 0); + if (!pos || pos < 0) return; + + if (target.nodeName !== "A") return; + + const node = view.state.doc.nodeAt(pos) as Node; + if (!node || !node.isAtom) return; + + // we need to check if any of the marks are links + const marks = node.marks; + + if (!marks) return; + + const linkMark = marks.find((mark) => mark.type.name === "link"); + + if (!linkMark) return; + + if (floatingElementRef.current) { + floatingElementRef.current?.remove(); + } + + if (cleanup) cleanup(); + + const href = linkMark.attrs.href; + const componentLink = new ReactRenderer(LinkView, { + props: { + view: "LinkPreview", + url: href, + editor: editor, + from: pos, + to: pos + node.nodeSize, + }, + editor, + }); + + const referenceElement = target as HTMLElement; + const floatingElement = componentLink.element as HTMLElement; + + floatingElementRef.current = floatingElement; + + const cleanupFunc = autoUpdate(referenceElement, floatingElement, () => { + computePosition(referenceElement, floatingElement, { + placement: "bottom", + middleware: [ + flip(), + shift(), + hide({ + strategy: "referenceHidden", + }), + ], + }).then(({ x, y }) => { + setCoordinates({ x: x - 300, y: y - 50 }); + setIsOpen(true); + setLinkViewProps({ + onActionCompleteHandler: props.onActionCompleteHandler, + closeLinkView: closeLinkView, + view: "LinkPreview", + url: href, + editor: editor, + from: pos, + to: pos + node.nodeSize, + }); + }); + }); + + setcleanup(cleanupFunc); + }, + [editor, cleanup] + ); + return ( -
+
{!readonly ? ( handlePageTitleChange(e.target.value)} @@ -52,11 +180,20 @@ export const PageRenderer = (props: IPageRenderer) => { disabled /> )} -
+
+ {isOpen && linkViewProps && coordinates && ( +
+ +
+ )}
); }; diff --git a/packages/editor/document-editor/src/ui/index.tsx b/packages/editor/document-editor/src/ui/index.tsx index df3554024..34aa54c50 100644 --- a/packages/editor/document-editor/src/ui/index.tsx +++ b/packages/editor/document-editor/src/ui/index.tsx @@ -151,6 +151,7 @@ const DocumentEditor = ({
-
+
Promise.resolve()} readonly={true} editor={editor} diff --git a/packages/editor/extensions/src/extensions/drag-drop.tsx b/packages/editor/extensions/src/extensions/drag-drop.tsx index 269caad93..af99fec61 100644 --- a/packages/editor/extensions/src/extensions/drag-drop.tsx +++ b/packages/editor/extensions/src/extensions/drag-drop.tsx @@ -3,6 +3,7 @@ import { Extension } from "@tiptap/core"; import { PluginKey, NodeSelection, Plugin } from "@tiptap/pm/state"; // @ts-ignore import { __serializeForClipboard, EditorView } from "@tiptap/pm/view"; +import React from "react"; function createDragHandleElement(): HTMLElement { const dragHandleElement = document.createElement("div"); @@ -30,6 +31,7 @@ function createDragHandleElement(): HTMLElement { export interface DragHandleOptions { dragHandleWidth: number; + setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void; } function absoluteRect(node: Element) { @@ -43,22 +45,23 @@ function absoluteRect(node: Element) { } function nodeDOMAtCoords(coords: { x: number; y: number }) { - return document.elementsFromPoint(coords.x, coords.y).find((elem: Element) => { - return ( - elem.parentElement?.matches?.(".ProseMirror") || - elem.matches( - [ - "li", - "p:not(:first-child)", - "pre", - "blockquote", - "h1, h2, h3", - "[data-type=horizontalRule]", - ".tableWrapper", - ].join(", ") - ) + return document + .elementsFromPoint(coords.x, coords.y) + .find( + (elem: Element) => + elem.parentElement?.matches?.(".ProseMirror") || + elem.matches( + [ + "li", + "p:not(:first-child)", + "pre", + "blockquote", + "h1, h2, h3", + "[data-type=horizontalRule]", + ".tableWrapper", + ].join(", ") + ) ); - }); } function nodePosAtDOM(node: Element, view: EditorView) { @@ -150,6 +153,8 @@ function DragHandle(options: DragHandleOptions) { } } + options.setHideDragHandle?.(hideDragHandle); + return new Plugin({ key: new PluginKey("dragHandle"), view: (view) => { @@ -237,14 +242,16 @@ function DragHandle(options: DragHandleOptions) { }); } -export const DragAndDrop = Extension.create({ - name: "dragAndDrop", +export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void) => + Extension.create({ + name: "dragAndDrop", - addProseMirrorPlugins() { - return [ - DragHandle({ - dragHandleWidth: 24, - }), - ]; - }, -}); + addProseMirrorPlugins() { + return [ + DragHandle({ + dragHandleWidth: 24, + setHideDragHandle, + }), + ]; + }, + }); diff --git a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx index 1e81c8173..3d1da6cda 100644 --- a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx @@ -1,14 +1,15 @@ -import { SlashCommand, DragAndDrop } from "@plane/editor-extensions"; -import Placeholder from "@tiptap/extension-placeholder"; import { UploadImage } from "@plane/editor-core"; +import { DragAndDrop, SlashCommand } from "@plane/editor-extensions"; +import Placeholder from "@tiptap/extension-placeholder"; export const RichTextEditorExtensions = ( uploadFile: UploadImage, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void, - dragDropEnabled?: boolean + dragDropEnabled?: boolean, + setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void ) => [ SlashCommand(uploadFile, setIsSubmitting), - dragDropEnabled === true && DragAndDrop, + dragDropEnabled === true && DragAndDrop(setHideDragHandle), Placeholder.configure({ placeholder: ({ node }) => { if (node.type.name === "heading") { diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index 17d701600..43c3f8f34 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -1,5 +1,4 @@ "use client"; -import * as React from "react"; import { DeleteImage, EditorContainer, @@ -10,8 +9,9 @@ import { UploadImage, useEditor, } from "@plane/editor-core"; -import { EditorBubbleMenu } from "src/ui/menus/bubble-menu"; +import * as React from "react"; import { RichTextEditorExtensions } from "src/ui/extensions"; +import { EditorBubbleMenu } from "src/ui/menus/bubble-menu"; export type IRichTextEditor = { value: string; @@ -66,6 +66,14 @@ const RichTextEditor = ({ rerenderOnPropsChange, mentionSuggestions, }: RichTextEditorProps) => { + const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = React.useState<() => void>(() => {}); + + // this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin + // loads such that we can invoke it from react when the cursor leaves the container + const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => { + setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop); + }; + const editor = useEditor({ onChange, debouncedUpdatesEnabled, @@ -78,7 +86,7 @@ const RichTextEditor = ({ restoreFile, forwardedRef, rerenderOnPropsChange, - extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting, dragDropEnabled), + extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting, dragDropEnabled, setHideDragHandleFunction), mentionHighlights, mentionSuggestions, }); @@ -92,7 +100,7 @@ const RichTextEditor = ({ if (!editor) return null; return ( - + {editor && }
diff --git a/packages/tailwind-config-custom/tailwind.config.js b/packages/tailwind-config-custom/tailwind.config.js index 97f7cab84..3465b8196 100644 --- a/packages/tailwind-config-custom/tailwind.config.js +++ b/packages/tailwind-config-custom/tailwind.config.js @@ -27,6 +27,7 @@ module.exports = { "custom-shadow-xl": "var(--color-shadow-xl)", "custom-shadow-2xl": "var(--color-shadow-2xl)", "custom-shadow-3xl": "var(--color-shadow-3xl)", + "custom-shadow-4xl": "var(--color-shadow-4xl)", "custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)", "custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)", "custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)", @@ -36,8 +37,8 @@ module.exports = { "custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)", "custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)", "custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)", - "onbording-shadow-sm": "var(--color-onboarding-shadow-sm)", - + "custom-sidebar-shadow-4xl": "var(--color-sidebar-shadow-4xl)", + "onboarding-shadow-sm": "var(--color-onboarding-shadow-sm)", }, colors: { custom: { @@ -212,7 +213,7 @@ module.exports = { to: { left: "100%" }, }, }, - typography: ({ theme }) => ({ + typography: () => ({ brand: { css: { "--tw-prose-body": convertToRGB("--color-text-100"), @@ -225,12 +226,12 @@ module.exports = { "--tw-prose-bullets": convertToRGB("--color-text-100"), "--tw-prose-hr": convertToRGB("--color-text-100"), "--tw-prose-quotes": convertToRGB("--color-text-100"), - "--tw-prose-quote-borders": convertToRGB("--color-border"), + "--tw-prose-quote-borders": convertToRGB("--color-border-200"), "--tw-prose-code": convertToRGB("--color-text-100"), "--tw-prose-pre-code": convertToRGB("--color-text-100"), "--tw-prose-pre-bg": convertToRGB("--color-background-100"), - "--tw-prose-th-borders": convertToRGB("--color-border"), - "--tw-prose-td-borders": convertToRGB("--color-border"), + "--tw-prose-th-borders": convertToRGB("--color-border-200"), + "--tw-prose-td-borders": convertToRGB("--color-border-200"), }, }, }), diff --git a/packages/types/src/cycles.d.ts b/packages/types/src/cycles.d.ts index 6723b3946..92ee18a42 100644 --- a/packages/types/src/cycles.d.ts +++ b/packages/types/src/cycles.d.ts @@ -1,4 +1,11 @@ -import type { IUser, TIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "@plane/types"; +import type { + IUser, + TIssue, + IProjectLite, + IWorkspaceLite, + IIssueFilterOptions, + IUserLite, +} from "@plane/types"; export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; @@ -40,6 +47,7 @@ export interface ICycle { }; workspace: string; workspace_detail: IWorkspaceLite; + issues?: TIssue[]; } export type TAssigneesDistribution = { @@ -80,9 +88,13 @@ export interface CycleIssueResponse { sub_issues_count: number; } -export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined; +export type SelectCycleType = + | (ICycle & { actionType: "edit" | "delete" | "create-issue" }) + | undefined; -export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | null; +export type SelectIssue = + | (TIssue & { actionType: "edit" | "delete" | "create" }) + | null; export type CycleDateCheckData = { start_date: string; diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts new file mode 100644 index 000000000..31751c0d0 --- /dev/null +++ b/packages/types/src/dashboard.d.ts @@ -0,0 +1,175 @@ +import { IIssueActivity, TIssuePriorities } from "./issues"; +import { TIssue } from "./issues/issue"; +import { TIssueRelationTypes } from "./issues/issue_relation"; +import { TStateGroups } from "./state"; + +export type TWidgetKeys = + | "overview_stats" + | "assigned_issues" + | "created_issues" + | "issues_by_state_groups" + | "issues_by_priority" + | "recent_activity" + | "recent_projects" + | "recent_collaborators"; + +export type TIssuesListTypes = "upcoming" | "overdue" | "completed"; + +export type TDurationFilterOptions = + | "today" + | "this_week" + | "this_month" + | "this_year"; + +// widget filters +export type TAssignedIssuesWidgetFilters = { + target_date?: TDurationFilterOptions; + tab?: TIssuesListTypes; +}; + +export type TCreatedIssuesWidgetFilters = { + target_date?: TDurationFilterOptions; + tab?: TIssuesListTypes; +}; + +export type TIssuesByStateGroupsWidgetFilters = { + target_date?: TDurationFilterOptions; +}; + +export type TIssuesByPriorityWidgetFilters = { + target_date?: TDurationFilterOptions; +}; + +export type TWidgetFiltersFormData = + | { + widgetKey: "assigned_issues"; + filters: Partial; + } + | { + widgetKey: "created_issues"; + filters: Partial; + } + | { + widgetKey: "issues_by_state_groups"; + filters: Partial; + } + | { + widgetKey: "issues_by_priority"; + filters: Partial; + }; + +export type TWidget = { + id: string; + is_visible: boolean; + key: TWidgetKeys; + readonly widget_filters: // only for read + TAssignedIssuesWidgetFilters & + TCreatedIssuesWidgetFilters & + TIssuesByStateGroupsWidgetFilters & + TIssuesByPriorityWidgetFilters; + filters: // only for write + TAssignedIssuesWidgetFilters & + TCreatedIssuesWidgetFilters & + TIssuesByStateGroupsWidgetFilters & + TIssuesByPriorityWidgetFilters; +}; + +export type TWidgetStatsRequestParams = + | { + widget_key: TWidgetKeys; + } + | { + target_date: string; + issue_type: TIssuesListTypes; + widget_key: "assigned_issues"; + expand?: "issue_relation"; + } + | { + target_date: string; + issue_type: TIssuesListTypes; + widget_key: "created_issues"; + } + | { + target_date: string; + widget_key: "issues_by_state_groups"; + } + | { + target_date: string; + widget_key: "issues_by_priority"; + }; + +export type TWidgetIssue = TIssue & { + issue_relation: { + id: string; + project_id: string; + relation_type: TIssueRelationTypes; + sequence_id: number; + }[]; +}; + +// widget stats responses +export type TOverviewStatsWidgetResponse = { + assigned_issues_count: number; + completed_issues_count: number; + created_issues_count: number; + pending_issues_count: number; +}; + +export type TAssignedIssuesWidgetResponse = { + issues: TWidgetIssue[]; + count: number; +}; + +export type TCreatedIssuesWidgetResponse = { + issues: TWidgetIssue[]; + count: number; +}; + +export type TIssuesByStateGroupsWidgetResponse = { + count: number; + state: TStateGroups; +}; + +export type TIssuesByPriorityWidgetResponse = { + count: number; + priority: TIssuePriorities; +}; + +export type TRecentActivityWidgetResponse = IIssueActivity; + +export type TRecentProjectsWidgetResponse = string[]; + +export type TRecentCollaboratorsWidgetResponse = { + active_issue_count: number; + user_id: string; +}; + +export type TWidgetStatsResponse = + | TOverviewStatsWidgetResponse + | TIssuesByStateGroupsWidgetResponse[] + | TIssuesByPriorityWidgetResponse[] + | TAssignedIssuesWidgetResponse + | TCreatedIssuesWidgetResponse + | TRecentActivityWidgetResponse[] + | TRecentProjectsWidgetResponse + | TRecentCollaboratorsWidgetResponse[]; + +// dashboard +export type TDashboard = { + created_at: string; + created_by: string | null; + description_html: string; + id: string; + identifier: string | null; + is_default: boolean; + name: string; + owned_by: string; + type: string; + updated_at: string; + updated_by: string | null; +}; + +export type THomeDashboardResponse = { + dashboard: TDashboard; + widgets: TWidget[]; +}; diff --git a/packages/types/src/inbox.d.ts b/packages/types/src/inbox.d.ts index 1b474c3ab..4d666ae83 100644 --- a/packages/types/src/inbox.d.ts +++ b/packages/types/src/inbox.d.ts @@ -1,7 +1,13 @@ -import { TIssue } from "./issues"; +import { TIssue } from "./issues/base"; import type { IProjectLite } from "./projects"; -export interface IInboxIssue extends TIssue { +export type TInboxIssueExtended = { + completed_at: string | null; + start_date: string | null; + target_date: string | null; +}; + +export interface IInboxIssue extends TIssue, TInboxIssueExtended { issue_inbox: { duplicate_to: string | null; id: string; @@ -48,7 +54,12 @@ interface StatusDuplicate { duplicate_to: string; } -export type TInboxStatus = StatusReject | StatusSnoozed | StatusAccepted | StatusDuplicate | StatePending; +export type TInboxStatus = + | StatusReject + | StatusSnoozed + | StatusAccepted + | StatusDuplicate + | StatePending; export interface IInboxFilterOptions { priority?: string[] | null; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 4bbed28d3..209aa6794 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -1,6 +1,7 @@ export * from "./users"; export * from "./workspace"; export * from "./cycles"; +export * from "./dashboard"; export * from "./projects"; export * from "./state"; export * from "./invitation"; diff --git a/packages/types/src/issues.d.ts b/packages/types/src/issues.d.ts index c0ad7bc7f..da6db8062 100644 --- a/packages/types/src/issues.d.ts +++ b/packages/types/src/issues.d.ts @@ -1,8 +1,6 @@ import { ReactElement } from "react"; import { KeyedMutator } from "swr"; import type { - IState, - IUser, ICycle, IModule, IUserLite, @@ -11,7 +9,7 @@ import type { IStateLite, Properties, IIssueDisplayFilterOptions, - IIssueReaction, + TIssue, } from "@plane/types"; export interface IIssueCycle { @@ -78,59 +76,6 @@ export interface IssueRelation { relation: "blocking" | null; } -export interface IIssue { - archived_at: string; - assignees: string[]; - assignee_details: IUser[]; - attachment_count: number; - attachments: any[]; - issue_relations: IssueRelation[]; - issue_reactions: IIssueReaction[]; - related_issues: IssueRelation[]; - bridge_id?: string | null; - completed_at: Date; - created_at: string; - created_by: string; - cycle: string | null; - cycle_id: string | null; - cycle_detail: ICycle | null; - description: any; - description_html: any; - description_stripped: any; - estimate_point: number | null; - id: string; - // tempId is used for optimistic updates. It is not a part of the API response. - tempId?: string; - issue_cycle: IIssueCycle | null; - issue_link: ILinkDetails[]; - issue_module: IIssueModule | null; - labels: string[]; - label_details: any[]; - is_draft: boolean; - links_list: IIssueLink[]; - link_count: number; - module: string | null; - module_id: string | null; - name: string; - parent: string | null; - parent_detail: IIssueParent | null; - priority: TIssuePriorities; - project: string; - project_detail: IProjectLite; - sequence_id: number; - sort_order: number; - sprints: string | null; - start_date: string | null; - state: string; - state_detail: IState; - sub_issues_count: number; - target_date: string | null; - updated_at: string; - updated_by: string; - workspace: string; - workspace_detail: IWorkspaceLite; -} - export interface ISubIssuesState { backlog: number; unstarted: number; @@ -276,69 +221,10 @@ export type GroupByColumnTypes = export interface IGroupByColumn { id: string; name: string; - Icon: ReactElement | undefined; + icon: ReactElement | undefined; payload: Partial; } export interface IIssueMap { [key: string]: TIssue; } - -// new issue structure types -export type TIssue = { - id: string; - name: string; - state_id: string; - description_html: string; - sort_order: number; - completed_at: string | null; - estimate_point: number | null; - priority: TIssuePriorities; - start_date: string | null; - target_date: string | null; - sequence_id: number; - project_id: string; - parent_id: string | null; - cycle_id: string | null; - module_id: string | null; - label_ids: string[]; - assignee_ids: string[]; - sub_issues_count: number; - created_at: string; - updated_at: string; - created_by: string; - updated_by: string; - attachment_count: number; - link_count: number; - is_subscribed: boolean; - archived_at: boolean; - is_draft: boolean; - // tempId is used for optimistic updates. It is not a part of the API response. - tempId?: string; - // issue details - related_issues: any; - issue_reactions: any; - issue_relations: any; - issue_cycle: any; - issue_module: any; - parent_detail: any; - issue_link: any; -}; - -export type TIssueMap = { - [issue_id: string]: TIssue; -}; - -export type TLoader = "init-loader" | "mutation" | undefined; - -export type TGroupedIssues = { - [group_id: string]: string[]; -}; - -export type TSubGroupedIssues = { - [sub_grouped_id: string]: { - [group_id: string]: string[]; - }; -}; - -export type TUnGroupedIssues = string[]; diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index e9ec14528..9734f85c2 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -1,32 +1,41 @@ +import { TIssuePriorities } from "../issues"; + // new issue structure types export type TIssue = { id: string; + sequence_id: number; name: string; - state_id: string; description_html: string; sort_order: number; - completed_at: string | null; - estimate_point: number | null; + + state_id: string; priority: TIssuePriorities; - start_date: string; - target_date: string; - sequence_id: number; + label_ids: string[]; + assignee_ids: string[]; + estimate_point: number | null; + + sub_issues_count: number; + attachment_count: number; + link_count: number; + project_id: string; parent_id: string | null; cycle_id: string | null; module_id: string | null; - label_ids: string[]; - assignee_ids: string[]; - sub_issues_count: number; + created_at: string; updated_at: string; + start_date: string | null; + target_date: string | null; + completed_at: string | null; + archived_at: string | null; + created_by: string; updated_by: string; - attachment_count: number; - link_count: number; - is_subscribed: boolean; - archived_at: boolean; + is_draft: boolean; + is_subscribed: boolean; + // tempId is used for optimistic updates. It is not a part of the API response. tempId?: string; }; diff --git a/packages/types/src/issues/issue_reaction.d.ts b/packages/types/src/issues/issue_reaction.d.ts index 2fe646246..6fc071a9f 100644 --- a/packages/types/src/issues/issue_reaction.d.ts +++ b/packages/types/src/issues/issue_reaction.d.ts @@ -17,5 +17,5 @@ export type TIssueReactionMap = { }; export type TIssueReactionIdMap = { - [issue_id: string]: string[]; + [issue_id: string]: { [reaction: string]: string[] }; }; diff --git a/packages/types/src/issues/issue_relation.d.ts b/packages/types/src/issues/issue_relation.d.ts index 0d959ff6b..0b1c5f7cd 100644 --- a/packages/types/src/issues/issue_relation.d.ts +++ b/packages/types/src/issues/issue_relation.d.ts @@ -6,12 +6,7 @@ export type TIssueRelationTypes = | "duplicate" | "relates_to"; -export type TIssueRelationObject = { issue_detail: TIssue }; - -export type TIssueRelation = Record< - TIssueRelationTypes, - TIssueRelationObject[] ->; +export type TIssueRelation = Record; export type TIssueRelationMap = { [issue_id: string]: Record; diff --git a/packages/types/src/issues/issue_sub_issues.d.ts b/packages/types/src/issues/issue_sub_issues.d.ts index 76dcf1288..e604761ed 100644 --- a/packages/types/src/issues/issue_sub_issues.d.ts +++ b/packages/types/src/issues/issue_sub_issues.d.ts @@ -1,11 +1,11 @@ import { TIssue } from "./issue"; export type TSubIssuesStateDistribution = { - backlog: number; - unstarted: number; - started: number; - completed: number; - cancelled: number; + backlog: string[]; + unstarted: string[]; + started: string[]; + completed: string[]; + cancelled: string[]; }; export type TIssueSubIssues = { diff --git a/packages/types/src/projects.d.ts b/packages/types/src/projects.d.ts index a412180b8..9c963258b 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -1,5 +1,11 @@ import { EUserProjectRoles } from "constants/project"; -import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from "."; +import type { + IUser, + IUserLite, + IWorkspace, + IWorkspaceLite, + TStateGroups, +} from "."; export interface IProject { archive_in: number; @@ -52,6 +58,11 @@ export interface IProjectLite { id: string; name: string; identifier: string; + emoji: string | null; + icon_prop: { + name: string; + color: string; + } | null; } type ProjectPreferences = { diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index 282fc5a9c..7f1d49632 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -1,4 +1,9 @@ -export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart"; +export type TIssueLayouts = + | "list" + | "kanban" + | "calendar" + | "spreadsheet" + | "gantt_chart"; export type TIssueGroupByOptions = | "state" @@ -108,10 +113,16 @@ export interface IIssueDisplayProperties { updated_on?: boolean; } +export type TIssueKanbanFilters = { + group_by: string[]; + sub_group_by: string[]; +}; + export interface IIssueFilters { filters: IIssueFilterOptions | undefined; displayFilters: IIssueDisplayFilterOptions | undefined; displayProperties: IIssueDisplayProperties | undefined; + kanbanFilters: TIssueKanbanFilters | undefined; } export interface IIssueFiltersResponse { diff --git a/packages/types/src/workspace-views.d.ts b/packages/types/src/workspace-views.d.ts index 29aa56742..e270f4f69 100644 --- a/packages/types/src/workspace-views.d.ts +++ b/packages/types/src/workspace-views.d.ts @@ -29,4 +29,8 @@ export interface IWorkspaceView { }; } -export type TStaticViewTypes = "all-issues" | "assigned" | "created" | "subscribed"; +export type TStaticViewTypes = + | "all-issues" + | "assigned" + | "created" + | "subscribed"; diff --git a/packages/types/src/workspace.d.ts b/packages/types/src/workspace.d.ts index 2fc8d6912..2d7e94d95 100644 --- a/packages/types/src/workspace.d.ts +++ b/packages/types/src/workspace.d.ts @@ -1,5 +1,10 @@ import { EUserWorkspaceRoles } from "constants/workspace"; -import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "@plane/types"; +import type { + IProjectMember, + IUser, + IUserLite, + IWorkspaceViewProps, +} from "@plane/types"; export interface IWorkspace { readonly id: string; @@ -32,8 +37,7 @@ export interface IWorkspaceMemberInvitation { responded_at: Date; role: EUserWorkspaceRoles; token: string; - workspace: string; - workspace_detail: { + workspace: { id: string; logo: string; name: string; diff --git a/packages/ui/helpers.ts b/packages/ui/helpers.ts new file mode 100644 index 000000000..a500a7385 --- /dev/null +++ b/packages/ui/helpers.ts @@ -0,0 +1,4 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); diff --git a/packages/ui/package.json b/packages/ui/package.json index b643d47d4..b8a669631 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -17,6 +17,17 @@ "lint": "eslint src/", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, + "dependencies": { + "@blueprintjs/core": "^4.16.3", + "@blueprintjs/popover2": "^1.13.3", + "@headlessui/react": "^1.7.17", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "react-color": "^2.19.3", + "react-dom": "^18.2.0", + "react-popper": "^2.3.0", + "tailwind-merge": "^2.0.0" + }, "devDependencies": { "@types/node": "^20.5.2", "@types/react": "^18.2.42", @@ -29,13 +40,5 @@ "tsconfig": "*", "tsup": "^5.10.1", "typescript": "4.7.4" - }, - "dependencies": { - "@blueprintjs/core": "^4.16.3", - "@blueprintjs/popover2": "^1.13.3", - "@headlessui/react": "^1.7.17", - "@popperjs/core": "^2.11.8", - "react-color": "^2.19.3", - "react-popper": "^2.3.0" } } diff --git a/packages/ui/src/avatar/avatar.tsx b/packages/ui/src/avatar/avatar.tsx index 4be345961..6344dce83 100644 --- a/packages/ui/src/avatar/avatar.tsx +++ b/packages/ui/src/avatar/avatar.tsx @@ -141,6 +141,7 @@ export const Avatar: React.FC = (props) => { } : {} } + tabIndex={-1} > {src ? ( {name} diff --git a/packages/ui/src/button/button.tsx b/packages/ui/src/button/button.tsx index d63d89eb2..10ee815f6 100644 --- a/packages/ui/src/button/button.tsx +++ b/packages/ui/src/button/button.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { getIconStyling, getButtonStyling, TButtonVariant, TButtonSizes } from "./helper"; +import { cn } from "../../helpers"; export interface ButtonProps extends React.ButtonHTMLAttributes { variant?: TButtonVariant; @@ -31,7 +32,7 @@ const Button = React.forwardRef((props, ref) => const buttonIconStyle = getIconStyling(size); return ( - @@ -83,86 +106,89 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80" } ${buttonClassName}`} + onClick={openDropdown} > {label} {!noChevron && !disabled &&
diff --git a/web/components/command-palette/actions/workspace-settings-actions.tsx b/web/components/command-palette/actions/workspace-settings-actions.tsx index 84e62593a..7503343ee 100644 --- a/web/components/command-palette/actions/workspace-settings-actions.tsx +++ b/web/components/command-palette/actions/workspace-settings-actions.tsx @@ -2,6 +2,7 @@ import { useRouter } from "next/router"; import { Command } from "cmdk"; // icons import { SettingIcon } from "components/icons"; +import Link from "next/link"; type Props = { closePalette: () => void; @@ -13,48 +14,55 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC = (props) = const router = useRouter(); const { workspaceSlug } = router.query; - const redirect = (path: string) => { - closePalette(); - router.push(path); - }; - return ( <> - redirect(`/${workspaceSlug}/settings`)} className="focus:outline-none"> -
- - General -
+ + +
+ + General +
+
- redirect(`/${workspaceSlug}/settings/members`)} className="focus:outline-none"> -
- - Members -
+ + +
+ + Members +
+
- redirect(`/${workspaceSlug}/settings/billing`)} className="focus:outline-none"> -
- - Billing and Plans -
+ + +
+ + Billing and Plans +
+
- redirect(`/${workspaceSlug}/settings/integrations`)} className="focus:outline-none"> -
- - Integrations -
+ + +
+ + Integrations +
+
- redirect(`/${workspaceSlug}/settings/imports`)} className="focus:outline-none"> -
- - Import -
+ + +
+ + Import +
+
- redirect(`/${workspaceSlug}/settings/exports`)} className="focus:outline-none"> -
- - Export -
+ + +
+ + Export +
+
); diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index 04b2fb714..6a550e0ad 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -60,6 +60,7 @@ export const CommandPalette: FC = observer(() => { isDeleteIssueModalOpen, toggleDeleteIssueModal, isAnyModalOpen, + createIssueStoreType, } = commandPalette; const { setToastAlert } = useToast(); @@ -216,6 +217,7 @@ export const CommandPalette: FC = observer(() => { isOpen={isCreateIssueModalOpen} onClose={() => toggleCreateIssueModal(false)} data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_id: moduleId.toString() } : undefined} + storeType={createIssueStoreType} /> {workspaceSlug && projectId && issueId && issueDetails && ( diff --git a/web/components/common/new-empty-state.tsx b/web/components/common/new-empty-state.tsx index dbe654e11..efbab8249 100644 --- a/web/components/common/new-empty-state.tsx +++ b/web/components/common/new-empty-state.tsx @@ -43,7 +43,7 @@ export const NewEmptyState: React.FC = ({ return (
-
+

{title}

{description &&

{description}

}
diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index 9f8023833..a5ffd807a 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -4,10 +4,11 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; import { useDropzone } from "react-dropzone"; -import { Tab, Transition, Popover } from "@headlessui/react"; +import { Tab, Popover } from "@headlessui/react"; import { Control, Controller } from "react-hook-form"; // hooks import { useApplication, useWorkspace } from "hooks/store"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; // services import { FileService } from "services/file.service"; // hooks @@ -38,13 +39,14 @@ type Props = { control: Control; onChange: (data: string) => void; disabled?: boolean; + tabIndex?: number; }; // services const fileService = new FileService(); export const ImagePickerPopover: React.FC = observer((props) => { - const { label, value, control, onChange, disabled = false } = props; + const { label, value, control, onChange, disabled = false, tabIndex } = props; // states const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); @@ -128,27 +130,27 @@ export const ImagePickerPopover: React.FC = observer((props) => { onChange(unsplashImages[0].urls.regular); }, [value, onChange, unsplashImages]); - useOutsideClickDetector(imagePickerRef, () => setIsOpen(false)); + const openDropdown = () => setIsOpen(true); + const closeDropdown = () => setIsOpen(false); + const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); + + useOutsideClickDetector(ref, closeDropdown); return ( - + setIsOpen((prev) => !prev)} + onClick={openDropdown} disabled={disabled} > {label} - - + + {isOpen && ( +
= observer((props) => {
-
+ )}
); }); diff --git a/web/components/core/index.ts b/web/components/core/index.ts index ff0fabc4e..4f99f3606 100644 --- a/web/components/core/index.ts +++ b/web/components/core/index.ts @@ -3,5 +3,4 @@ export * from "./modals"; export * from "./sidebar"; export * from "./theme"; export * from "./activity"; -export * from "./reaction-selector"; export * from "./image-picker-popover"; diff --git a/web/components/core/sidebar/progress-chart.tsx b/web/components/core/sidebar/progress-chart.tsx index 9e9a4bac8..3d47d8eca 100644 --- a/web/components/core/sidebar/progress-chart.tsx +++ b/web/components/core/sidebar/progress-chart.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { eachDayOfInterval } from "date-fns"; +import { eachDayOfInterval, isValid } from "date-fns"; // ui import { LineGraph } from "components/ui"; // helpers @@ -41,13 +41,19 @@ const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) => )); const ProgressChart: React.FC = ({ distribution, startDate, endDate, totalIssues }) => { - const chartData = Object.keys(distribution).map((key) => ({ + const chartData = Object.keys(distribution ?? []).map((key) => ({ currentDate: renderFormattedDateWithoutYear(key), pending: distribution[key], })); const generateXAxisTickValues = () => { - const dates = eachDayOfInterval({ start: new Date(startDate), end: new Date(endDate) }); + const start = new Date(startDate); + const end = new Date(endDate); + + let dates: Date[] = []; + if (isValid(start) && isValid(end)) { + dates = eachDayOfInterval({ start, end }); + } const maxDates = 4; const totalDates = dates.length; diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 6d89981cd..c37cdf4b9 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -126,7 +126,7 @@ export const SidebarProgressStats: React.FC = ({ - {distribution.assignees.length > 0 ? ( + {distribution?.assignees.length > 0 ? ( distribution.assignees.map((assignee, index) => { if (assignee.assignee_id) return ( @@ -183,7 +183,7 @@ export const SidebarProgressStats: React.FC = ({ )} - {distribution.labels.length > 0 ? ( + {distribution?.labels.length > 0 ? ( distribution.labels.map((label, index) => ( = observer((props) => { - // router - const router = useRouter(); + // props const { workspaceSlug, projectId } = props; - + // store hooks const { - issues: { issues }, + issues: { issues, fetchActiveCycleIssues }, issueMap, } = useIssues(EIssuesStoreType.CYCLE); - // store hooks const { commandPalette: { toggleCreateCycleModal }, } = useApplication(); @@ -99,13 +97,14 @@ export const ActiveCycleDetails: React.FC = observer((props const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null; const issueIds = issues?.[ACTIVE_CYCLE_ISSUES]; - // useSWR( - // workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId, { priority: "urgent,high" }) : null, - // workspaceSlug && projectId && cycleId - // ? () => - // fetchActiveCycleIssues(workspaceSlug, projectId, ) - // : null - // ); + useSWR( + workspaceSlug && projectId && currentProjectActiveCycleId + ? CYCLE_ISSUES_WITH_PARAMS(currentProjectActiveCycleId, { priority: "urgent,high" }) + : null, + workspaceSlug && projectId && currentProjectActiveCycleId + ? () => fetchActiveCycleIssues(workspaceSlug, projectId, currentProjectActiveCycleId) + : null + ); if (!activeCycle && isLoading) return ( @@ -382,9 +381,9 @@ export const ActiveCycleDetails: React.FC = observer((props {issueIds ? ( issueIds.length > 0 ? ( issueIds.map((issue: any) => ( -
router.push(`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`)} + href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`} className="flex cursor-pointer flex-wrap items-center justify-between gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 px-3 py-1.5" >
@@ -427,7 +426,7 @@ export const ActiveCycleDetails: React.FC = observer((props )}
-
+ )) ) : (
@@ -465,7 +464,7 @@ export const ActiveCycleDetails: React.FC = observer((props { issueIds?.filter( (issueId) => - getProjectStates(issueMap[issueId]?.project_id).find( + getProjectStates(issueMap[issueId]?.project_id)?.find( (issue) => issue.id === issueMap[issueId]?.state_id )?.group === "completed" )?.length diff --git a/web/components/cycles/active-cycle-info.tsx b/web/components/cycles/active-cycle-info.tsx new file mode 100644 index 000000000..6c64f7c6b --- /dev/null +++ b/web/components/cycles/active-cycle-info.tsx @@ -0,0 +1,254 @@ +import { FC, MouseEvent, useCallback } from "react"; +import Link from "next/link"; +// ui +import { + AvatarGroup, + Tooltip, + LinearProgressIndicator, + ContrastIcon, + RunningIcon, + LayersIcon, + StateGroupIcon, + Avatar, +} from "@plane/ui"; +// components +import { SingleProgressStats } from "components/core"; +import { ActiveCycleProgressStats } from "./active-cycle-stats"; +// hooks +import { useCycle } from "hooks/store"; +import useToast from "hooks/use-toast"; +import useLocalStorage from "hooks/use-local-storage"; +// icons +import { ArrowRight, CalendarDays, Star, Target } from "lucide-react"; +// types +import { ICycle, TCycleLayout, TCycleView } from "@plane/types"; +// helpers +import { renderFormattedDate, findHowManyDaysLeft } from "helpers/date-time.helper"; +import { truncateText } from "helpers/string.helper"; +// constants +import { STATE_GROUPS_DETAILS } from "constants/cycle"; + +export type ActiveCycleInfoProps = { + cycle: ICycle; + workspaceSlug: string; + projectId: string; +}; + +export const ActiveCycleInfo: FC = (props) => { + const { cycle, workspaceSlug, projectId } = props; + + // store + const { addCycleToFavorites, removeCycleFromFavorites } = useCycle(); + // local storage + const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); + const { setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); + // toast alert + const { setToastAlert } = useToast(); + + const groupedIssues: any = { + backlog: cycle.backlog_issues, + unstarted: cycle.unstarted_issues, + started: cycle.started_issues, + completed: cycle.completed_issues, + cancelled: cycle.cancelled_issues, + }; + + const progressIndicatorData = STATE_GROUPS_DETAILS.map((group, index) => ({ + id: index, + name: group.title, + value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0, + color: group.color, + })); + + const handleCurrentLayout = useCallback( + (_layout: TCycleLayout) => { + setCycleLayout(_layout); + }, + [setCycleLayout] + ); + + const handleCurrentView = useCallback( + (_view: TCycleView) => { + setCycleTab(_view); + if (_view === "draft") handleCurrentLayout("list"); + }, + [handleCurrentLayout, setCycleTab] + ); + + const handleAddToFavorites = (e: MouseEvent) => { + e.preventDefault(); + if (!workspaceSlug || !projectId) return; + + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", + }); + }); + }; + + const handleRemoveFromFavorites = (e: MouseEvent) => { + e.preventDefault(); + if (!workspaceSlug || !projectId) return; + + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", + }); + }); + }; + + return ( +
+
+
+
+
+
+ + + + + +

{truncateText(cycle.name, 70)}

+
+
+ + + + + {findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left + + + {cycle.is_favorite ? ( + + ) : ( + + )} + +
+ +
+
+ + {cycle?.start_date && {renderFormattedDate(cycle?.start_date)}} +
+ +
+ + {cycle?.end_date && {renderFormattedDate(cycle?.end_date)}} +
+
+ +
+
+ {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( + {cycle.owned_by.display_name} + ) : ( + + {cycle.owned_by.display_name.charAt(0)} + + )} + {cycle.owned_by.display_name} +
+ + {cycle.assignees.length > 0 && ( +
+ + {cycle.assignees.map((assignee: any) => ( + + ))} + +
+ )} +
+ +
+
+ + {cycle.total_issues} issues +
+
+ + {cycle.completed_issues} issues +
+
+
+ { + handleCurrentView("active"); + }} + > + + View Cycle + + + + + + View Cycle Issues + + +
+
+
+
+
+
+
+
+ Progress + +
+
+ {Object.keys(groupedIssues).map((group, index) => ( + + + {group} +
+ } + completed={groupedIssues[group]} + total={cycle.total_issues} + /> + ))} +
+
+
+
+ +
+
+
+
+ ); +}; diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index bbb30bc7a..d25364bcd 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -116,7 +116,8 @@ export const CyclesListItem: FC = (props) => { if (!cycleDetails) return null; // computed - const cycleStatus = cycleDetails.status.toLocaleLowerCase() as TCycleGroups; + // TODO: change this logic once backend fix the response + const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft"; const isCompleted = cycleStatus === "completed"; const endDate = new Date(cycleDetails.end_date ?? ""); const startDate = new Date(cycleDetails.start_date ?? ""); diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 686937b71..87796340e 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -31,7 +31,12 @@ export const CyclesList: FC = observer((props) => {
{cycleIds.map((cycleId) => ( - + ))}
= (props) => { setActiveProject(val); }} buttonVariant="background-with-text" + tabIndex={7} /> )} /> @@ -89,6 +90,7 @@ export const CycleForm: React.FC = (props) => { inputSize="md" onChange={onChange} hasError={Boolean(errors?.name)} + tabIndex={1} /> )} /> @@ -106,6 +108,7 @@ export const CycleForm: React.FC = (props) => { hasError={Boolean(errors?.description)} value={value} onChange={onChange} + tabIndex={2} /> )} /> @@ -124,6 +127,7 @@ export const CycleForm: React.FC = (props) => { buttonVariant="border-with-text" placeholder="Start date" maxDate={maxDate ?? undefined} + tabIndex={3} />
)} @@ -140,6 +144,7 @@ export const CycleForm: React.FC = (props) => { buttonVariant="border-with-text" placeholder="End date" minDate={minDate} + tabIndex={4} />
)} @@ -149,10 +154,10 @@ export const CycleForm: React.FC = (props) => {
- -
diff --git a/web/components/cycles/index.ts b/web/components/cycles/index.ts index db5e9de9e..975a03188 100644 --- a/web/components/cycles/index.ts +++ b/web/components/cycles/index.ts @@ -15,3 +15,4 @@ export * from "./cycles-board-card"; export * from "./delete-modal"; export * from "./cycle-peek-overview"; export * from "./cycles-list-item"; +export * from "./active-cycle-info"; diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index f2f7792f6..81cefac50 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -539,7 +539,9 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
- Invalid date. Please enter valid date. + {cycleDetails?.start_date && cycleDetails?.end_date + ? "This cycle isn't active yet." + : "Invalid date. Please enter valid date."}
)} diff --git a/web/components/dashboard/home-dashboard-widgets.tsx b/web/components/dashboard/home-dashboard-widgets.tsx new file mode 100644 index 000000000..6402877c5 --- /dev/null +++ b/web/components/dashboard/home-dashboard-widgets.tsx @@ -0,0 +1,61 @@ +import { observer } from "mobx-react-lite"; +// hooks +import { useApplication, useDashboard } from "hooks/store"; +// components +import { + AssignedIssuesWidget, + CreatedIssuesWidget, + IssuesByPriorityWidget, + IssuesByStateGroupWidget, + OverviewStatsWidget, + RecentActivityWidget, + RecentCollaboratorsWidget, + RecentProjectsWidget, + WidgetProps, +} from "components/dashboard"; +// types +import { TWidgetKeys } from "@plane/types"; + +const WIDGETS_LIST: { + [key in TWidgetKeys]: { component: React.FC; fullWidth: boolean }; +} = { + overview_stats: { component: OverviewStatsWidget, fullWidth: true }, + assigned_issues: { component: AssignedIssuesWidget, fullWidth: false }, + created_issues: { component: CreatedIssuesWidget, fullWidth: false }, + issues_by_state_groups: { component: IssuesByStateGroupWidget, fullWidth: false }, + issues_by_priority: { component: IssuesByPriorityWidget, fullWidth: false }, + recent_activity: { component: RecentActivityWidget, fullWidth: false }, + recent_projects: { component: RecentProjectsWidget, fullWidth: false }, + recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true }, +}; + +export const DashboardWidgets = observer(() => { + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { homeDashboardId, homeDashboardWidgets } = useDashboard(); + + const doesWidgetExist = (widgetKey: TWidgetKeys) => + Boolean(homeDashboardWidgets?.find((widget) => widget.key === widgetKey)); + + if (!workspaceSlug || !homeDashboardId) return null; + + return ( +
+ {Object.entries(WIDGETS_LIST).map(([key, widget]) => { + const WidgetComponent = widget.component; + // if the widget doesn't exist, return null + if (!doesWidgetExist(key as TWidgetKeys)) return null; + // if the widget is full width, return it in a 2 column grid + if (widget.fullWidth) + return ( +
+ +
+ ); + else return ; + })} +
+ ); +}); diff --git a/web/components/dashboard/index.ts b/web/components/dashboard/index.ts new file mode 100644 index 000000000..129cdb69e --- /dev/null +++ b/web/components/dashboard/index.ts @@ -0,0 +1,3 @@ +export * from "./widgets"; +export * from "./home-dashboard-widgets"; +export * from "./project-empty-state"; diff --git a/web/components/dashboard/project-empty-state.tsx b/web/components/dashboard/project-empty-state.tsx new file mode 100644 index 000000000..c0ac90f34 --- /dev/null +++ b/web/components/dashboard/project-empty-state.tsx @@ -0,0 +1,41 @@ +import Image from "next/image"; +import { observer } from "mobx-react-lite"; +// hooks +import { useApplication, useUser } from "hooks/store"; +// ui +import { Button } from "@plane/ui"; +// assets +import ProjectEmptyStateImage from "public/empty-state/dashboard/project.svg"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; + +export const DashboardProjectEmptyState = observer(() => { + // store hooks + const { + commandPalette: { toggleCreateProjectModal }, + } = useApplication(); + const { + membership: { currentWorkspaceRole }, + } = useUser(); + // derived values + const canCreateProject = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + + return ( +
+

Overview of your projects, activity, and metrics

+

+ Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this + page will transform into a space that helps you progress. Admins will also see items which help their team + progress. +

+ Project empty state + {canCreateProject && ( +
+ +
+ )} +
+ ); +}); diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx new file mode 100644 index 000000000..5ad24ee0f --- /dev/null +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -0,0 +1,119 @@ +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react-lite"; +import { Tab } from "@headlessui/react"; +// hooks +import { useDashboard } from "hooks/store"; +// components +import { + DurationFilterDropdown, + TabsList, + WidgetIssuesList, + WidgetLoader, + WidgetProps, +} from "components/dashboard/widgets"; +// helpers +import { getCustomDates, getRedirectionFilters } from "helpers/dashboard.helper"; +// types +import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; +// constants +import { ISSUES_TABS_LIST } from "constants/dashboard"; + +const WIDGET_KEY = "assigned_issues"; + +export const AssignedIssuesWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [fetching, setFetching] = useState(false); + // store hooks + const { + fetchWidgetStats, + widgetDetails: allWidgetDetails, + widgetStats: allWidgetStats, + updateDashboardWidgetFilters, + } = useDashboard(); + // derived values + const widgetDetails = allWidgetDetails?.[workspaceSlug]?.[dashboardId]?.find((w) => w.key === WIDGET_KEY); + const widgetStats = allWidgetStats?.[workspaceSlug]?.[dashboardId]?.[WIDGET_KEY] as TAssignedIssuesWidgetResponse; + + const handleUpdateFilters = async (filters: Partial) => { + if (!widgetDetails) return; + + setFetching(true); + + await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { + widgetKey: WIDGET_KEY, + filters, + }); + + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + issue_type: widgetDetails.widget_filters.tab ?? "upcoming", + target_date: getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"), + expand: "issue_relation", + }).finally(() => setFetching(false)); + }; + + useEffect(() => { + if (!widgetDetails) return; + + const filterDates = getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"); + + if (!widgetStats) + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + issue_type: widgetDetails.widget_filters.tab ?? "upcoming", + target_date: filterDates, + expand: "issue_relation", + }); + }, [dashboardId, fetchWidgetStats, widgetDetails, widgetStats, workspaceSlug]); + + const filterParams = getRedirectionFilters(widgetDetails?.widget_filters.tab ?? "upcoming"); + const redirectionLink = `/${workspaceSlug}/workspace-views/assigned/${filterParams}`; + + if (!widgetDetails || !widgetStats) return ; + + return ( +
+ +

All issues assigned

+ + handleUpdateFilters({ + target_date: val, + }) + } + /> + + t.key === widgetDetails.widget_filters.tab ?? "upcoming")} + onChange={(i) => { + const selectedTab = ISSUES_TABS_LIST[i]; + handleUpdateFilters({ tab: selectedTab.key ?? "upcoming" }); + }} + className="h-full flex flex-col" + > +
+ +
+ + {ISSUES_TABS_LIST.map((tab) => ( + + + + ))} + +
+
+ ); +}); diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx new file mode 100644 index 000000000..c25623070 --- /dev/null +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react-lite"; +import { Tab } from "@headlessui/react"; +// hooks +import { useDashboard } from "hooks/store"; +// components +import { + DurationFilterDropdown, + TabsList, + WidgetIssuesList, + WidgetLoader, + WidgetProps, +} from "components/dashboard/widgets"; +// helpers +import { getCustomDates, getRedirectionFilters } from "helpers/dashboard.helper"; +// types +import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; +// constants +import { ISSUES_TABS_LIST } from "constants/dashboard"; + +const WIDGET_KEY = "created_issues"; + +export const CreatedIssuesWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [fetching, setFetching] = useState(false); + // store hooks + const { + fetchWidgetStats, + widgetDetails: allWidgetDetails, + widgetStats: allWidgetStats, + updateDashboardWidgetFilters, + } = useDashboard(); + // derived values + const widgetDetails = allWidgetDetails?.[workspaceSlug]?.[dashboardId]?.find((w) => w.key === WIDGET_KEY); + const widgetStats = allWidgetStats?.[workspaceSlug]?.[dashboardId]?.[WIDGET_KEY] as TCreatedIssuesWidgetResponse; + + const handleUpdateFilters = async (filters: Partial) => { + if (!widgetDetails) return; + + setFetching(true); + + await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { + widgetKey: WIDGET_KEY, + filters, + }); + + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + issue_type: widgetDetails.widget_filters.tab ?? "upcoming", + target_date: getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"), + }).finally(() => setFetching(false)); + }; + + useEffect(() => { + if (!widgetDetails) return; + + if (!widgetStats) + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + issue_type: widgetDetails.widget_filters.tab ?? "upcoming", + target_date: getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"), + }); + }, [dashboardId, fetchWidgetStats, widgetDetails, widgetStats, workspaceSlug]); + + const filterParams = getRedirectionFilters(widgetDetails?.widget_filters.tab ?? "upcoming"); + const redirectionLink = `/${workspaceSlug}/workspace-views/created/${filterParams}`; + + if (!widgetDetails || !widgetStats) return ; + + return ( +
+ +

All issues created

+ + handleUpdateFilters({ + target_date: val, + }) + } + /> + + t.key === widgetDetails.widget_filters.tab ?? "upcoming")} + onChange={(i) => { + const selectedTab = ISSUES_TABS_LIST[i]; + handleUpdateFilters({ tab: selectedTab.key ?? "upcoming" }); + }} + className="h-full flex flex-col" + > +
+ +
+ + {ISSUES_TABS_LIST.map((tab) => ( + + + + ))} + +
+
+ ); +}); diff --git a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx new file mode 100644 index 000000000..fedc92cbe --- /dev/null +++ b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx @@ -0,0 +1,41 @@ +import { ChevronDown } from "lucide-react"; +// ui +import { CustomMenu } from "@plane/ui"; +// types +import { TDurationFilterOptions } from "@plane/types"; +// constants +import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; + +type Props = { + onChange: (value: TDurationFilterOptions) => void; + value: TDurationFilterOptions; +}; + +export const DurationFilterDropdown: React.FC = (props) => { + const { onChange, value } = props; + + return ( + + {DURATION_FILTER_OPTIONS.find((option) => option.key === value)?.label} + +
+ } + placement="bottom-end" + > + {DURATION_FILTER_OPTIONS.map((option) => ( + { + e.preventDefault(); + e.stopPropagation(); + onChange(option.key); + }} + > + {option.label} + + ))} + + ); +}; diff --git a/web/components/dashboard/widgets/dropdowns/index.ts b/web/components/dashboard/widgets/dropdowns/index.ts new file mode 100644 index 000000000..cff4cdb44 --- /dev/null +++ b/web/components/dashboard/widgets/dropdowns/index.ts @@ -0,0 +1 @@ +export * from "./duration-filter"; diff --git a/web/components/dashboard/widgets/empty-states/assigned-issues.tsx b/web/components/dashboard/widgets/empty-states/assigned-issues.tsx new file mode 100644 index 000000000..ef85ff611 --- /dev/null +++ b/web/components/dashboard/widgets/empty-states/assigned-issues.tsx @@ -0,0 +1,42 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; +// constants +import { ASSIGNED_ISSUES_EMPTY_STATES } from "constants/dashboard"; + +type Props = { + filter: TDurationFilterOptions; + type: TIssuesListTypes; +}; + +export const AssignedIssuesEmptyState: React.FC = (props) => { + const { filter, type } = props; + // next-themes + const { resolvedTheme } = useTheme(); + + const typeDetails = ASSIGNED_ISSUES_EMPTY_STATES[type]; + + const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage; + + return ( +
+

{typeDetails.title(filter)}

+
+ Assigned issues +
+
+ ); +}; diff --git a/web/components/dashboard/widgets/empty-states/created-issues.tsx b/web/components/dashboard/widgets/empty-states/created-issues.tsx new file mode 100644 index 000000000..3b5a646bb --- /dev/null +++ b/web/components/dashboard/widgets/empty-states/created-issues.tsx @@ -0,0 +1,42 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; +// constants +import { CREATED_ISSUES_EMPTY_STATES } from "constants/dashboard"; + +type Props = { + filter: TDurationFilterOptions; + type: TIssuesListTypes; +}; + +export const CreatedIssuesEmptyState: React.FC = (props) => { + const { filter, type } = props; + // next-themes + const { resolvedTheme } = useTheme(); + + const typeDetails = CREATED_ISSUES_EMPTY_STATES[type]; + + const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage; + + return ( +
+

{typeDetails.title(filter)}

+
+ Created issues +
+
+ ); +}; diff --git a/web/components/dashboard/widgets/empty-states/index.ts b/web/components/dashboard/widgets/empty-states/index.ts new file mode 100644 index 000000000..72ca1dbb2 --- /dev/null +++ b/web/components/dashboard/widgets/empty-states/index.ts @@ -0,0 +1,6 @@ +export * from "./assigned-issues"; +export * from "./created-issues"; +export * from "./issues-by-priority"; +export * from "./issues-by-state-group"; +export * from "./recent-activity"; +export * from "./recent-collaborators"; diff --git a/web/components/dashboard/widgets/empty-states/issues-by-priority.tsx b/web/components/dashboard/widgets/empty-states/issues-by-priority.tsx new file mode 100644 index 000000000..41e2754d5 --- /dev/null +++ b/web/components/dashboard/widgets/empty-states/issues-by-priority.tsx @@ -0,0 +1,45 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// assets +import DarkImage from "public/empty-state/dashboard/dark/issues-by-priority.svg"; +import LightImage from "public/empty-state/dashboard/light/issues-by-priority.svg"; +// helpers +import { cn } from "helpers/common.helper"; +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +// types +import { TDurationFilterOptions } from "@plane/types"; + +type Props = { + filter: TDurationFilterOptions; +}; + +export const IssuesByPriorityEmptyState: React.FC = (props) => { + const { filter } = props; + // next-themes + const { resolvedTheme } = useTheme(); + + return ( +
+

+ No assigned issues {replaceUnderscoreIfSnakeCase(filter)}. +

+
+ Issues by priority +
+
+ ); +}; diff --git a/web/components/dashboard/widgets/empty-states/issues-by-state-group.tsx b/web/components/dashboard/widgets/empty-states/issues-by-state-group.tsx new file mode 100644 index 000000000..166dbb36f --- /dev/null +++ b/web/components/dashboard/widgets/empty-states/issues-by-state-group.tsx @@ -0,0 +1,45 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// assets +import DarkImage from "public/empty-state/dashboard/dark/issues-by-state-group.svg"; +import LightImage from "public/empty-state/dashboard/light/issues-by-state-group.svg"; +// helpers +import { cn } from "helpers/common.helper"; +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +// types +import { TDurationFilterOptions } from "@plane/types"; + +type Props = { + filter: TDurationFilterOptions; +}; + +export const IssuesByStateGroupEmptyState: React.FC = (props) => { + const { filter } = props; + // next-themes + const { resolvedTheme } = useTheme(); + + return ( +
+

+ No assigned issues {replaceUnderscoreIfSnakeCase(filter)}. +

+
+ Issues by state group +
+
+ ); +}; diff --git a/web/components/dashboard/widgets/empty-states/recent-activity.tsx b/web/components/dashboard/widgets/empty-states/recent-activity.tsx new file mode 100644 index 000000000..ea62fbf08 --- /dev/null +++ b/web/components/dashboard/widgets/empty-states/recent-activity.tsx @@ -0,0 +1,42 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// assets +import DarkImage from "public/empty-state/dashboard/dark/recent-activity.svg"; +import LightImage from "public/empty-state/dashboard/light/recent-activity.svg"; +// helpers +import { cn } from "helpers/common.helper"; + +type Props = {}; + +export const RecentActivityEmptyState: React.FC = (props) => { + const {} = props; + // next-themes + const { resolvedTheme } = useTheme(); + + return ( +
+

+ Feels new, go and explore our tool in depth and come back +
+ to see your activity. +

+
+ Issues by priority +
+
+ ); +}; diff --git a/web/components/dashboard/widgets/empty-states/recent-collaborators.tsx b/web/components/dashboard/widgets/empty-states/recent-collaborators.tsx new file mode 100644 index 000000000..0ab0db1f9 --- /dev/null +++ b/web/components/dashboard/widgets/empty-states/recent-collaborators.tsx @@ -0,0 +1,40 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// assets +import DarkImage from "public/empty-state/dashboard/dark/recent-collaborators.svg"; +import LightImage from "public/empty-state/dashboard/light/recent-collaborators.svg"; +// helpers +import { cn } from "helpers/common.helper"; + +type Props = {}; + +export const RecentCollaboratorsEmptyState: React.FC = (props) => { + const {} = props; + // next-themes + const { resolvedTheme } = useTheme(); + + return ( +
+

+ People are excited to work with you, once they do you will find your frequent collaborators here. +

+
+ Recent collaborators +
+
+ ); +}; diff --git a/web/components/dashboard/widgets/index.ts b/web/components/dashboard/widgets/index.ts new file mode 100644 index 000000000..a481a8881 --- /dev/null +++ b/web/components/dashboard/widgets/index.ts @@ -0,0 +1,12 @@ +export * from "./dropdowns"; +export * from "./empty-states"; +export * from "./issue-panels"; +export * from "./loaders"; +export * from "./assigned-issues"; +export * from "./created-issues"; +export * from "./issues-by-priority"; +export * from "./issues-by-state-group"; +export * from "./overview-stats"; +export * from "./recent-activity"; +export * from "./recent-collaborators"; +export * from "./recent-projects"; diff --git a/web/components/dashboard/widgets/issue-panels/index.ts b/web/components/dashboard/widgets/issue-panels/index.ts new file mode 100644 index 000000000..f5b7d53d4 --- /dev/null +++ b/web/components/dashboard/widgets/issue-panels/index.ts @@ -0,0 +1,3 @@ +export * from "./issue-list-item"; +export * from "./issues-list"; +export * from "./tabs-list"; diff --git a/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx b/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx new file mode 100644 index 000000000..3da862d91 --- /dev/null +++ b/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx @@ -0,0 +1,297 @@ +import { observer } from "mobx-react-lite"; +import isToday from "date-fns/isToday"; +// hooks +import { useIssueDetail, useMember, useProject } from "hooks/store"; +// ui +import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui"; +// helpers +import { findTotalDaysInRange, renderFormattedDate } from "helpers/date-time.helper"; +// types +import { TIssue, TWidgetIssue } from "@plane/types"; + +export type IssueListItemProps = { + issueId: string; + onClick: (issue: TIssue) => void; + workspaceSlug: string; +}; + +export const AssignedUpcomingIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getProjectById } = useProject(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + // derived values + const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined; + + if (!issueDetails) return null; + + const projectDetails = getProjectById(issueDetails.project_id); + + const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? []; + + const blockedByIssueProjectDetails = + blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null; + + return ( + onClick(issueDetails)} + className="py-2 px-3 hover:bg-custom-background-80 rounded grid grid-cols-6 gap-1" + > +
+ + + {projectDetails?.identifier} {issueDetails.sequence_id} + +
{issueDetails.name}
+
+
+ {issueDetails.target_date + ? isToday(new Date(issueDetails.target_date)) + ? "Today" + : renderFormattedDate(issueDetails.target_date) + : "-"} +
+
+ {blockedByIssues.length > 0 + ? blockedByIssues.length > 1 + ? `${blockedByIssues.length} blockers` + : `${blockedByIssueProjectDetails?.identifier} ${blockedByIssues[0]?.sequence_id}` + : "-"} +
+
+ ); +}); + +export const AssignedOverdueIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getProjectById } = useProject(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + // derived values + const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined; + + if (!issueDetails) return null; + + const projectDetails = getProjectById(issueDetails.project_id); + const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? []; + + const blockedByIssueProjectDetails = + blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null; + + const dueBy = findTotalDaysInRange(new Date(issueDetails.target_date ?? ""), new Date(), false); + + return ( + onClick(issueDetails)} + className="py-2 px-3 hover:bg-custom-background-80 rounded grid grid-cols-6 gap-1" + > +
+ + + {projectDetails?.identifier} {issueDetails.sequence_id} + +
{issueDetails.name}
+
+
+ {dueBy} {`day${dueBy > 1 ? "s" : ""}`} +
+
+ {blockedByIssues.length > 0 + ? blockedByIssues.length > 1 + ? `${blockedByIssues.length} blockers` + : `${blockedByIssueProjectDetails?.identifier} ${blockedByIssues[0]?.sequence_id}` + : "-"} +
+
+ ); +}); + +export const AssignedCompletedIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectById } = useProject(); + // derived values + const issueDetails = getIssueById(issueId); + + if (!issueDetails) return null; + + const projectDetails = getProjectById(issueDetails.project_id); + + return ( + onClick(issueDetails)} + className="py-2 px-3 hover:bg-custom-background-80 rounded grid grid-cols-6 gap-1" + > +
+ + + {projectDetails?.identifier} {issueDetails.sequence_id} + +
{issueDetails.name}
+
+
+ ); +}); + +export const CreatedUpcomingIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getUserDetails } = useMember(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectById } = useProject(); + // derived values + const issue = getIssueById(issueId); + + if (!issue) return null; + + const projectDetails = getProjectById(issue.project_id); + + return ( + onClick(issue)} + className="py-2 px-3 hover:bg-custom-background-80 rounded grid grid-cols-6 gap-1" + > +
+ + + {projectDetails?.identifier} {issue.sequence_id} + +
{issue.name}
+
+
+ {issue.target_date + ? isToday(new Date(issue.target_date)) + ? "Today" + : renderFormattedDate(issue.target_date) + : "-"} +
+
+ {issue.assignee_ids.length > 0 ? ( + + {issue.assignee_ids?.map((assigneeId) => { + const userDetails = getUserDetails(assigneeId); + + if (!userDetails) return null; + + return ; + })} + + ) : ( + "-" + )} +
+
+ ); +}); + +export const CreatedOverdueIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getUserDetails } = useMember(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectById } = useProject(); + // derived values + const issue = getIssueById(issueId); + + if (!issue) return null; + + const projectDetails = getProjectById(issue.project_id); + + const dueBy = findTotalDaysInRange(new Date(issue.target_date ?? ""), new Date(), false); + + return ( + onClick(issue)} + className="py-2 px-3 hover:bg-custom-background-80 rounded grid grid-cols-6 gap-1" + > +
+ + + {projectDetails?.identifier} {issue.sequence_id} + +
{issue.name}
+
+
+ {dueBy} {`day${dueBy > 1 ? "s" : ""}`} +
+
+ {issue.assignee_ids.length > 0 ? ( + + {issue.assignee_ids?.map((assigneeId) => { + const userDetails = getUserDetails(assigneeId); + + if (!userDetails) return null; + + return ; + })} + + ) : ( + "-" + )} +
+
+ ); +}); + +export const CreatedCompletedIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getUserDetails } = useMember(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectById } = useProject(); + // derived values + const issue = getIssueById(issueId); + + if (!issue) return null; + + const projectDetails = getProjectById(issue.project_id); + + return ( + onClick(issue)} + className="py-2 px-3 hover:bg-custom-background-80 rounded grid grid-cols-6 gap-1" + > +
+ + + {projectDetails?.identifier} {issue.sequence_id} + +
{issue.name}
+
+
+ {issue.assignee_ids.length > 0 ? ( + + {issue.assignee_ids?.map((assigneeId) => { + const userDetails = getUserDetails(assigneeId); + + if (!userDetails) return null; + + return ; + })} + + ) : ( + "-" + )} +
+
+ ); +}); diff --git a/web/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/components/dashboard/widgets/issue-panels/issues-list.tsx new file mode 100644 index 000000000..d104bbd05 --- /dev/null +++ b/web/components/dashboard/widgets/issue-panels/issues-list.tsx @@ -0,0 +1,124 @@ +import Link from "next/link"; +// hooks +import { useIssueDetail } from "hooks/store"; +// components +import { + AssignedCompletedIssueListItem, + AssignedIssuesEmptyState, + AssignedOverdueIssueListItem, + AssignedUpcomingIssueListItem, + CreatedCompletedIssueListItem, + CreatedIssuesEmptyState, + CreatedOverdueIssueListItem, + CreatedUpcomingIssueListItem, + IssueListItemProps, +} from "components/dashboard/widgets"; +// ui +import { Loader, getButtonStyling } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +import { getRedirectionFilters } from "helpers/dashboard.helper"; +// types +import { TDurationFilterOptions, TIssue, TIssuesListTypes } from "@plane/types"; + +export type WidgetIssuesListProps = { + filter: TDurationFilterOptions | undefined; + isLoading: boolean; + issues: TIssue[]; + tab: TIssuesListTypes; + totalIssues: number; + type: "assigned" | "created"; + workspaceSlug: string; +}; + +export const WidgetIssuesList: React.FC = (props) => { + const { filter, isLoading, issues, tab, totalIssues, type, workspaceSlug } = props; + // store hooks + const { setPeekIssue } = useIssueDetail(); + + const handleIssuePeekOverview = (issue: TIssue) => + setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); + + const filterParams = getRedirectionFilters(tab); + + const ISSUE_LIST_ITEM: { + [key in string]: { + [key in TIssuesListTypes]: React.FC; + }; + } = { + assigned: { + upcoming: AssignedUpcomingIssueListItem, + overdue: AssignedOverdueIssueListItem, + completed: AssignedCompletedIssueListItem, + }, + created: { + upcoming: CreatedUpcomingIssueListItem, + overdue: CreatedOverdueIssueListItem, + completed: CreatedCompletedIssueListItem, + }, + }; + + return ( + <> +
+ {isLoading ? ( + + + + + + + ) : issues.length > 0 ? ( + <> +
+
+ Issues + + {totalIssues} + +
+ {tab === "upcoming" &&
Due date
} + {tab === "overdue" &&
Due by
} + {type === "assigned" && tab !== "completed" &&
Blocked by
} + {type === "created" &&
Assigned to
} +
+
+ {issues.map((issue) => { + const IssueListItem = ISSUE_LIST_ITEM[type][tab]; + + if (!IssueListItem) return null; + + return ( + + ); + })} +
+ + ) : ( +
+ {type === "assigned" && } + {type === "created" && } +
+ )} +
+ {totalIssues > issues.length && ( + + View all issues + + )} + + ); +}; diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx new file mode 100644 index 000000000..6ef6ec0ee --- /dev/null +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -0,0 +1,26 @@ +import { Tab } from "@headlessui/react"; +// helpers +import { cn } from "helpers/common.helper"; +// constants +import { ISSUES_TABS_LIST } from "constants/dashboard"; + +export const TabsList = () => ( + + {ISSUES_TABS_LIST.map((tab) => ( + + cn("font-semibold text-xs rounded py-1.5 focus:outline-none", { + "bg-custom-background-100 text-custom-text-300 shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selected, + "text-custom-text-400": !selected, + }) + } + > + {tab.label} + + ))} + +); diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx new file mode 100644 index 000000000..1e0cd30da --- /dev/null +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -0,0 +1,208 @@ +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react-lite"; +// hooks +import { useDashboard } from "hooks/store"; +// components +import { MarimekkoGraph } from "components/ui"; +import { + DurationFilterDropdown, + IssuesByPriorityEmptyState, + WidgetLoader, + WidgetProps, +} from "components/dashboard/widgets"; +// ui +import { PriorityIcon } from "@plane/ui"; +// helpers +import { getCustomDates } from "helpers/dashboard.helper"; +// types +import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; +// constants +import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; +import { ISSUE_PRIORITIES } from "constants/issue"; + +const TEXT_COLORS = { + urgent: "#F4A9AA", + high: "#AB4800", + medium: "#AB6400", + low: "#1F2D5C", + none: "#60646C", +}; + +const CustomBar = (props: any) => { + const { bar, workspaceSlug } = props; + // states + const [isMouseOver, setIsMouseOver] = useState(false); + + return ( + + setIsMouseOver(true)} + onMouseLeave={() => setIsMouseOver(false)} + > + + + {bar?.id} + + + + ); +}; + +const WIDGET_KEY = "issues_by_priority"; + +export const IssuesByPriorityWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { + fetchWidgetStats, + widgetDetails: allWidgetDetails, + widgetStats: allWidgetStats, + updateDashboardWidgetFilters, + } = useDashboard(); + const widgetDetails = allWidgetDetails?.[workspaceSlug]?.[dashboardId]?.find((w) => w.key === WIDGET_KEY); + const widgetStats = allWidgetStats?.[workspaceSlug]?.[dashboardId]?.[WIDGET_KEY] as TIssuesByPriorityWidgetResponse[]; + + const handleUpdateFilters = async (filters: Partial) => { + if (!widgetDetails) return; + + await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { + widgetKey: WIDGET_KEY, + filters, + }); + + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + target_date: getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"), + }); + }; + + useEffect(() => { + if (!widgetDetails) return; + + if (!widgetStats) + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + target_date: getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"), + }); + }, [dashboardId, fetchWidgetStats, widgetDetails, widgetStats, workspaceSlug]); + + if (!widgetDetails || !widgetStats) return ; + + const totalCount = widgetStats.reduce((acc, item) => acc + item?.count, 0); + const chartData = widgetStats + .filter((i) => i.count !== 0) + .map((item) => ({ + priority: item?.priority, + percentage: (item?.count / totalCount) * 100, + urgent: item?.priority === "urgent" ? 1 : 0, + high: item?.priority === "high" ? 1 : 0, + medium: item?.priority === "medium" ? 1 : 0, + low: item?.priority === "low" ? 1 : 0, + none: item?.priority === "none" ? 1 : 0, + })); + + const CustomBarsLayer = (props: any) => { + const { bars } = props; + + return ( + + {bars + ?.filter((b: any) => b?.value === 1) // render only bars with value 1 + .map((bar: any) => ( + + ))} + + ); + }; + + return ( + +
+

Priority of assigned issues

+ + handleUpdateFilters({ + target_date: val, + }) + } + /> +
+ {totalCount > 0 ? ( +
+
+ ({ + id: p.key, + value: p.key, + }))} + axisBottom={null} + axisLeft={null} + height="119px" + margin={{ + top: 11, + right: 0, + bottom: 0, + left: 0, + }} + defs={PRIORITY_GRAPH_GRADIENTS} + fill={ISSUE_PRIORITIES.map((p) => ({ + match: { + id: p.key, + }, + id: `gradient${p.title}`, + }))} + tooltip={() => <>} + enableGridX={false} + enableGridY={false} + layers={[CustomBarsLayer]} + /> +
+ {chartData.map((item) => ( +

+ + {item.percentage.toFixed(0)}% +

+ ))} +
+
+
+ ) : ( +
+ +
+ )} + + ); +}); diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx new file mode 100644 index 000000000..d4478040a --- /dev/null +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -0,0 +1,188 @@ +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// hooks +import { useDashboard } from "hooks/store"; +// components +import { PieGraph } from "components/ui"; +import { + DurationFilterDropdown, + IssuesByStateGroupEmptyState, + WidgetLoader, + WidgetProps, +} from "components/dashboard/widgets"; +// helpers +import { getCustomDates } from "helpers/dashboard.helper"; +// types +import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; +// constants +import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; +import { STATE_GROUPS } from "constants/state"; + +const WIDGET_KEY = "issues_by_state_groups"; + +export const IssuesByStateGroupWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [activeStateGroup, setActiveStateGroup] = useState("started"); + // router + const router = useRouter(); + // store hooks + const { + fetchWidgetStats, + widgetDetails: allWidgetDetails, + widgetStats: allWidgetStats, + updateDashboardWidgetFilters, + } = useDashboard(); + // derived values + const widgetDetails = allWidgetDetails?.[workspaceSlug]?.[dashboardId]?.find((w) => w.key === WIDGET_KEY); + const widgetStats = allWidgetStats?.[workspaceSlug]?.[dashboardId]?.[ + WIDGET_KEY + ] as TIssuesByStateGroupsWidgetResponse[]; + + const handleUpdateFilters = async (filters: Partial) => { + if (!widgetDetails) return; + + await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { + widgetKey: WIDGET_KEY, + filters, + }); + + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + target_date: getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"), + }); + }; + + useEffect(() => { + if (!widgetDetails) return; + + if (!widgetStats) + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + target_date: getCustomDates(widgetDetails.widget_filters.target_date ?? "this_week"), + }); + }, [dashboardId, fetchWidgetStats, widgetDetails, widgetStats, workspaceSlug]); + + if (!widgetDetails || !widgetStats) return ; + + const totalCount = widgetStats?.reduce((acc, item) => acc + item?.count, 0); + const chartData = widgetStats?.map((item) => ({ + color: STATE_GROUP_GRAPH_COLORS[item?.state as keyof typeof STATE_GROUP_GRAPH_COLORS], + id: item?.state, + label: item?.state, + value: (item?.count / totalCount) * 100, + })); + + const CenteredMetric = ({ dataWithArc, centerX, centerY }: any) => { + const data = dataWithArc?.find((datum: any) => datum?.id === activeStateGroup); + const percentage = chartData?.find((item) => item.id === activeStateGroup)?.value?.toFixed(0); + + return ( + + + {percentage}% + + + {data?.id} + + + ); + }; + + return ( + +
+

State of assigned issues

+ + handleUpdateFilters({ + target_date: val, + }) + } + /> +
+ {totalCount > 0 ? ( +
+
+
+ datum.data.color} + padAngle={1} + enableArcLinkLabels={false} + enableArcLabels={false} + activeOuterRadiusOffset={5} + tooltip={() => <>} + margin={{ + top: 0, + right: 5, + bottom: 0, + left: 5, + }} + defs={STATE_GROUP_GRAPH_GRADIENTS} + fill={Object.values(STATE_GROUPS).map((p) => ({ + match: { + id: p.key, + }, + id: `gradient${p.label}`, + }))} + onClick={(datum, e) => { + e.preventDefault(); + e.stopPropagation(); + router.push(`/${workspaceSlug}/workspace-views/assigned/?state_group=${datum.id}`); + }} + onMouseEnter={(datum) => setActiveStateGroup(datum.id as TStateGroups)} + layers={["arcs", CenteredMetric]} + /> +
+
+ {chartData.map((item) => ( +
+
+
+ {item.label} +
+ {item.value.toFixed(0)}% +
+ ))} +
+
+
+ ) : ( +
+ +
+ )} + + ); +}); diff --git a/web/components/dashboard/widgets/loaders/assigned-issues.tsx b/web/components/dashboard/widgets/loaders/assigned-issues.tsx new file mode 100644 index 000000000..4de381b29 --- /dev/null +++ b/web/components/dashboard/widgets/loaders/assigned-issues.tsx @@ -0,0 +1,22 @@ +// ui +import { Loader } from "@plane/ui"; + +export const AssignedIssuesWidgetLoader = () => ( + +
+ + +
+
+ + +
+
+ + + + + +
+
+); diff --git a/web/components/dashboard/widgets/loaders/index.ts b/web/components/dashboard/widgets/loaders/index.ts new file mode 100644 index 000000000..ee5286f0f --- /dev/null +++ b/web/components/dashboard/widgets/loaders/index.ts @@ -0,0 +1 @@ +export * from "./loader"; diff --git a/web/components/dashboard/widgets/loaders/issues-by-priority.tsx b/web/components/dashboard/widgets/loaders/issues-by-priority.tsx new file mode 100644 index 000000000..4051a2908 --- /dev/null +++ b/web/components/dashboard/widgets/loaders/issues-by-priority.tsx @@ -0,0 +1,15 @@ +// ui +import { Loader } from "@plane/ui"; + +export const IssuesByPriorityWidgetLoader = () => ( + + +
+ + + + + +
+
+); diff --git a/web/components/dashboard/widgets/loaders/issues-by-state-group.tsx b/web/components/dashboard/widgets/loaders/issues-by-state-group.tsx new file mode 100644 index 000000000..d2316802d --- /dev/null +++ b/web/components/dashboard/widgets/loaders/issues-by-state-group.tsx @@ -0,0 +1,21 @@ +// ui +import { Loader } from "@plane/ui"; + +export const IssuesByStateGroupWidgetLoader = () => ( + + +
+
+
+ +
+
+
+
+ {Array.from({ length: 5 }).map((_, index) => ( + + ))} +
+
+ +); diff --git a/web/components/dashboard/widgets/loaders/loader.tsx b/web/components/dashboard/widgets/loaders/loader.tsx new file mode 100644 index 000000000..141bb5533 --- /dev/null +++ b/web/components/dashboard/widgets/loaders/loader.tsx @@ -0,0 +1,31 @@ +// components +import { AssignedIssuesWidgetLoader } from "./assigned-issues"; +import { IssuesByPriorityWidgetLoader } from "./issues-by-priority"; +import { IssuesByStateGroupWidgetLoader } from "./issues-by-state-group"; +import { OverviewStatsWidgetLoader } from "./overview-stats"; +import { RecentActivityWidgetLoader } from "./recent-activity"; +import { RecentProjectsWidgetLoader } from "./recent-projects"; +import { RecentCollaboratorsWidgetLoader } from "./recent-collaborators"; +// types +import { TWidgetKeys } from "@plane/types"; + +type Props = { + widgetKey: TWidgetKeys; +}; + +export const WidgetLoader: React.FC = (props) => { + const { widgetKey } = props; + + const loaders = { + overview_stats: , + assigned_issues: , + created_issues: , + issues_by_state_groups: , + issues_by_priority: , + recent_activity: , + recent_projects: , + recent_collaborators: , + }; + + return loaders[widgetKey]; +}; diff --git a/web/components/dashboard/widgets/loaders/overview-stats.tsx b/web/components/dashboard/widgets/loaders/overview-stats.tsx new file mode 100644 index 000000000..f72d66ce4 --- /dev/null +++ b/web/components/dashboard/widgets/loaders/overview-stats.tsx @@ -0,0 +1,13 @@ +// ui +import { Loader } from "@plane/ui"; + +export const OverviewStatsWidgetLoader = () => ( + + {Array.from({ length: 4 }).map((_, index) => ( +
+ + +
+ ))} +
+); diff --git a/web/components/dashboard/widgets/loaders/recent-activity.tsx b/web/components/dashboard/widgets/loaders/recent-activity.tsx new file mode 100644 index 000000000..47e895a6e --- /dev/null +++ b/web/components/dashboard/widgets/loaders/recent-activity.tsx @@ -0,0 +1,19 @@ +// ui +import { Loader } from "@plane/ui"; + +export const RecentActivityWidgetLoader = () => ( + + + {Array.from({ length: 7 }).map((_, index) => ( +
+
+ +
+
+ + +
+
+ ))} +
+); diff --git a/web/components/dashboard/widgets/loaders/recent-collaborators.tsx b/web/components/dashboard/widgets/loaders/recent-collaborators.tsx new file mode 100644 index 000000000..d838967af --- /dev/null +++ b/web/components/dashboard/widgets/loaders/recent-collaborators.tsx @@ -0,0 +1,18 @@ +// ui +import { Loader } from "@plane/ui"; + +export const RecentCollaboratorsWidgetLoader = () => ( + + +
+ {Array.from({ length: 8 }).map((_, index) => ( +
+
+ +
+ +
+ ))} +
+
+); diff --git a/web/components/dashboard/widgets/loaders/recent-projects.tsx b/web/components/dashboard/widgets/loaders/recent-projects.tsx new file mode 100644 index 000000000..fc181ffab --- /dev/null +++ b/web/components/dashboard/widgets/loaders/recent-projects.tsx @@ -0,0 +1,19 @@ +// ui +import { Loader } from "@plane/ui"; + +export const RecentProjectsWidgetLoader = () => ( + + + {Array.from({ length: 5 }).map((_, index) => ( +
+
+ +
+
+ + +
+
+ ))} +
+); diff --git a/web/components/dashboard/widgets/overview-stats.tsx b/web/components/dashboard/widgets/overview-stats.tsx new file mode 100644 index 000000000..74630e1f8 --- /dev/null +++ b/web/components/dashboard/widgets/overview-stats.tsx @@ -0,0 +1,93 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +// hooks +import { useDashboard } from "hooks/store"; +// components +import { WidgetLoader } from "components/dashboard/widgets"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; +// types +import { TOverviewStatsWidgetResponse } from "@plane/types"; + +export type WidgetProps = { + dashboardId: string; + workspaceSlug: string; +}; + +const WIDGET_KEY = "overview_stats"; + +export const OverviewStatsWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { fetchWidgetStats, widgetStats: allWidgetStats } = useDashboard(); + // derived values + const widgetStats = allWidgetStats?.[workspaceSlug]?.[dashboardId]?.[WIDGET_KEY] as TOverviewStatsWidgetResponse; + + const today = renderFormattedPayloadDate(new Date()); + const STATS_LIST = [ + { + key: "assigned", + title: "Issues assigned", + count: widgetStats?.assigned_issues_count, + link: `/${workspaceSlug}/workspace-views/assigned`, + }, + { + key: "overdue", + title: "Issues overdue", + count: widgetStats?.pending_issues_count, + link: `/${workspaceSlug}/workspace-views/assigned/?target_date=${today};before`, + }, + { + key: "created", + title: "Issues created", + count: widgetStats?.created_issues_count, + link: `/${workspaceSlug}/workspace-views/created`, + }, + { + key: "completed", + title: "Issues completed", + count: widgetStats?.completed_issues_count, + link: `/${workspaceSlug}/workspace-views/assigned?state_group=completed`, + }, + ]; + + useEffect(() => { + if (!widgetStats) + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + }, [dashboardId, fetchWidgetStats, widgetStats, workspaceSlug]); + + if (!widgetStats) return ; + + return ( +
+ {STATS_LIST.map((stat, index) => { + const isFirst = index === 0; + const isLast = index === STATS_LIST.length - 1; + const isMiddle = !isFirst && !isLast; + + return ( +
+ {!isLast && ( +
+ )} + +
{stat.count}
+

{stat.title}

+ +
+ ); + })} +
+ ); +}); diff --git a/web/components/dashboard/widgets/recent-activity.tsx b/web/components/dashboard/widgets/recent-activity.tsx new file mode 100644 index 000000000..52fca4600 --- /dev/null +++ b/web/components/dashboard/widgets/recent-activity.tsx @@ -0,0 +1,105 @@ +import { useEffect } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react-lite"; +import { History } from "lucide-react"; +// hooks +import { useDashboard, useUser } from "hooks/store"; +// components +import { ActivityIcon, ActivityMessage } from "components/core"; +import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; +// ui +import { Avatar } from "@plane/ui"; +// helpers +import { calculateTimeAgo } from "helpers/date-time.helper"; +// types +import { TRecentActivityWidgetResponse } from "@plane/types"; + +const WIDGET_KEY = "recent_activity"; + +export const RecentActivityWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { currentUser } = useUser(); + // derived values + const { fetchWidgetStats, widgetStats: allWidgetStats } = useDashboard(); + const widgetStats = allWidgetStats?.[workspaceSlug]?.[dashboardId]?.[WIDGET_KEY] as TRecentActivityWidgetResponse[]; + + useEffect(() => { + if (!widgetStats) + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + }, [dashboardId, fetchWidgetStats, widgetStats, workspaceSlug]); + + if (!widgetStats) return ; + + return ( + +
+

My activity

+
+ {widgetStats.length > 0 ? ( +
+ {widgetStats.map((activity) => ( +
+
+ {activity.field ? ( + activity.new_value === "restore" ? ( + + ) : ( +
+ +
+ ) + ) : activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( + + ) : ( +
+ {activity.actor_detail.is_bot + ? activity.actor_detail.first_name.charAt(0) + : activity.actor_detail.display_name.charAt(0)} +
+ )} +
+
+

+ + {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail.display_name}{" "} + + {activity.field ? ( + + ) : ( + + created this{" "} + + Issue. + + + )} +

+

{calculateTimeAgo(activity.created_at)}

+
+
+ ))} +
+ ) : ( +
+ +
+ )} + + ); +}); diff --git a/web/components/dashboard/widgets/recent-collaborators.tsx b/web/components/dashboard/widgets/recent-collaborators.tsx new file mode 100644 index 000000000..693f9808d --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators.tsx @@ -0,0 +1,93 @@ +import { useEffect } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react-lite"; +// hooks +import { useDashboard, useMember, useUser } from "hooks/store"; +// components +import { RecentCollaboratorsEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; +// ui +import { Avatar } from "@plane/ui"; +// types +import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; + +type CollaboratorListItemProps = { + issueCount: number; + userId: string; + workspaceSlug: string; +}; + +const WIDGET_KEY = "recent_collaborators"; + +const CollaboratorListItem: React.FC = observer((props) => { + const { issueCount, userId, workspaceSlug } = props; + // store hooks + const { currentUser } = useUser(); + const { getUserDetails } = useMember(); + // derived values + const userDetails = getUserDetails(userId); + const isCurrentUser = userId === currentUser?.id; + + if (!userDetails) return null; + + return ( + +
+ +
+
+ {isCurrentUser ? "You" : userDetails?.display_name} +
+

+ {issueCount} active issue{issueCount > 1 ? "s" : ""} +

+ + ); +}); + +export const RecentCollaboratorsWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { fetchWidgetStats, widgetStats: allWidgetStats } = useDashboard(); + const widgetStats = allWidgetStats?.[workspaceSlug]?.[dashboardId]?.[ + WIDGET_KEY + ] as TRecentCollaboratorsWidgetResponse[]; + + useEffect(() => { + if (!widgetStats) + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + }, [dashboardId, fetchWidgetStats, widgetStats, workspaceSlug]); + + if (!widgetStats) return ; + + return ( +
+
+

Collaborators

+
+ {widgetStats.length > 1 ? ( +
+ {widgetStats.map((user) => ( + + ))} +
+ ) : ( +
+ +
+ )} +
+ ); +}); diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx new file mode 100644 index 000000000..d85cf52c8 --- /dev/null +++ b/web/components/dashboard/widgets/recent-projects.tsx @@ -0,0 +1,125 @@ +import { useEffect } from "react"; +import Link from "next/link"; +import { observer } from "mobx-react-lite"; +import { Plus } from "lucide-react"; +// hooks +import { useApplication, useDashboard, useProject, useUser } from "hooks/store"; +// components +import { WidgetLoader, WidgetProps } from "components/dashboard/widgets"; +// ui +import { Avatar, AvatarGroup } from "@plane/ui"; +// helpers +import { renderEmoji } from "helpers/emoji.helper"; +// types +import { TRecentProjectsWidgetResponse } from "@plane/types"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; +import { PROJECT_BACKGROUND_COLORS } from "constants/dashboard"; + +const WIDGET_KEY = "recent_projects"; + +type ProjectListItemProps = { + projectId: string; + workspaceSlug: string; +}; + +const ProjectListItem: React.FC = observer((props) => { + const { projectId, workspaceSlug } = props; + // store hooks + const { getProjectById } = useProject(); + const projectDetails = getProjectById(projectId); + + const randomBgColor = PROJECT_BACKGROUND_COLORS[Math.floor(Math.random() * PROJECT_BACKGROUND_COLORS.length)]; + + if (!projectDetails) return null; + + return ( + +
+ {projectDetails.emoji ? ( + + {renderEmoji(projectDetails.emoji)} + + ) : projectDetails.icon_prop ? ( +
{renderEmoji(projectDetails.icon_prop)}
+ ) : ( + + {projectDetails.name.charAt(0)} + + )} +
+
+
+ {projectDetails.name} +
+
+ + {projectDetails.members?.map((member) => ( + + ))} + +
+
+ + ); +}); + +export const RecentProjectsWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { + commandPalette: { toggleCreateProjectModal }, + } = useApplication(); + const { + membership: { currentWorkspaceRole }, + } = useUser(); + const { fetchWidgetStats, widgetStats: allWidgetStats } = useDashboard(); + // derived values + const widgetStats = allWidgetStats?.[workspaceSlug]?.[dashboardId]?.[WIDGET_KEY] as TRecentProjectsWidgetResponse; + const canCreateProject = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + + useEffect(() => { + if (!widgetStats) + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + }, [dashboardId, fetchWidgetStats, widgetStats, workspaceSlug]); + + if (!widgetStats) return ; + + return ( + +
+

My projects

+
+
+ {canCreateProject && ( + + )} + {widgetStats.map((projectId) => ( + + ))} +
+ + ); +}); diff --git a/web/components/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx index 4d5c60acd..5a861a8f9 100644 --- a/web/components/dropdowns/cycle.tsx +++ b/web/components/dropdowns/cycle.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactNode, useEffect, useState } from "react"; +import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; @@ -6,6 +6,8 @@ import { Placement } from "@popperjs/core"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks import { useApplication, useCycle } from "hooks/store"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // icons import { ContrastIcon } from "@plane/ui"; // helpers @@ -26,6 +28,7 @@ type Props = { placement?: Placement; projectId: string; value: string | null; + tabIndex?: number; }; type ButtonProps = { @@ -104,9 +107,13 @@ export const CycleDropdown: React.FC = observer((props) => { placement, projectId, value, + tabIndex, } = props; // states const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + // refs + const dropdownRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -166,15 +173,26 @@ export const CycleDropdown: React.FC = observer((props) => { const selectedCycle = value ? getCycleById(value) : null; + const openDropdown = () => { + setIsOpen(true); + if (referenceElement) referenceElement.focus(); + }; + const closeDropdown = () => setIsOpen(false); + const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); + useOutsideClickDetector(dropdownRef, closeDropdown); + return ( {button ? ( @@ -182,6 +200,7 @@ export const CycleDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} + onClick={openDropdown} > {button} @@ -197,6 +216,7 @@ export const CycleDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} + onClick={openDropdown} > {buttonVariant === "border-with-text" ? ( = observer((props) => { )} - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) + {isOpen && ( + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matches found

+ ) ) : ( -

No matches found

- ) - ) : ( -

Loading...

- )} +

Loading...

+ )} +
-
- + + )} ); }); diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx index e791413b4..92f35b910 100644 --- a/web/components/dropdowns/date.tsx +++ b/web/components/dropdowns/date.tsx @@ -1,9 +1,12 @@ -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; import { Popover } from "@headlessui/react"; import DatePicker from "react-datepicker"; import { usePopper } from "react-popper"; import { CalendarDays, X } from "lucide-react"; // import "react-datepicker/dist/react-datepicker.css"; +// hooks +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; import { cn } from "helpers/common.helper"; @@ -25,6 +28,7 @@ type Props = { placement?: Placement; value: Date | string | null; closeOnSelect?: boolean; + tabIndex?: number; }; type ButtonProps = { @@ -124,7 +128,11 @@ export const DateDropdown: React.FC = (props) => { placement, value, closeOnSelect = true, + tabIndex, } = props; + const [isOpen, setIsOpen] = useState(false); + // refs + const dropdownRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -143,8 +151,16 @@ export const DateDropdown: React.FC = (props) => { const isDateSelected = value !== null && value !== undefined && value.toString().trim() !== ""; + const openDropdown = () => { + setIsOpen(true); + if (referenceElement) referenceElement.focus(); + }; + const closeDropdown = () => setIsOpen(false); + const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); + useOutsideClickDetector(dropdownRef, closeDropdown); + return ( - + {({ close }) => ( <> @@ -159,6 +175,7 @@ export const DateDropdown: React.FC = (props) => { }, buttonContainerClassName )} + onClick={openDropdown} > {buttonVariant === "border-with-text" ? ( = (props) => { ) : null} - -
- { - onChange(val); - if (closeOnSelect) close(); - }} - dateFormat="dd-MM-yyyy" - minDate={minDate} - maxDate={maxDate} - calendarClassName="shadow-custom-shadow-rg rounded" - inline - /> -
-
+ {isOpen && ( + +
+ { + onChange(val); + if (closeOnSelect) close(); + }} + dateFormat="dd-MM-yyyy" + minDate={minDate} + maxDate={maxDate} + calendarClassName="shadow-custom-shadow-rg rounded" + inline + /> +
+
+ )} )}
diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index 8d538f53f..9138b2bea 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactNode, useEffect, useState } from "react"; +import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; @@ -7,6 +7,8 @@ import { Check, ChevronDown, Search, Triangle } from "lucide-react"; import sortBy from "lodash/sortBy"; // hooks import { useApplication, useEstimate } from "hooks/store"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { cn } from "helpers/common.helper"; // types @@ -24,6 +26,7 @@ type Props = { placement?: Placement; projectId: string; value: number | null; + tabIndex?: number; }; type ButtonProps = { @@ -102,9 +105,13 @@ export const EstimateDropdown: React.FC = observer((props) => { placement, projectId, value, + tabIndex, } = props; // states const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + // refs + const dropdownRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -160,15 +167,26 @@ export const EstimateDropdown: React.FC = observer((props) => { const selectedEstimate = value !== null ? getEstimatePointValue(value) : null; + const openDropdown = () => { + setIsOpen(true); + if (referenceElement) referenceElement.focus(); + }; + const closeDropdown = () => setIsOpen(false); + const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); + useOutsideClickDetector(dropdownRef, closeDropdown); + return ( {button ? ( @@ -176,6 +194,7 @@ export const EstimateDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} + onClick={openDropdown} > {button} @@ -191,6 +210,7 @@ export const EstimateDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} + onClick={openDropdown} > {buttonVariant === "border-with-text" ? ( = observer((props) => { )} - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) + {isOpen && ( + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) ) : ( -

No matching results

- ) - ) : ( -

Loading...

- )} +

Loading...

+ )} +
-
- + + )} ); }); diff --git a/web/components/dropdowns/member/project-member.tsx b/web/components/dropdowns/member/project-member.tsx index 18d317a56..1e6856274 100644 --- a/web/components/dropdowns/member/project-member.tsx +++ b/web/components/dropdowns/member/project-member.tsx @@ -1,10 +1,12 @@ -import { Fragment, useEffect, useState } from "react"; +import { Fragment, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; import { Check, Search } from "lucide-react"; // hooks import { useApplication, useMember, useUser } from "hooks/store"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns"; // icons @@ -33,9 +35,13 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { placement, projectId, value, + tabIndex, } = props; // states const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + // refs + const dropdownRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -93,12 +99,23 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { if (!projectMemberIds) fetchProjectMembers(workspaceSlug, projectId); }, [fetchProjectMembers, projectId, projectMemberIds, workspaceSlug]); + const openDropdown = () => { + setIsOpen(true); + if (referenceElement) referenceElement.focus(); + }; + const closeDropdown = () => setIsOpen(false); + const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); + useOutsideClickDetector(dropdownRef, closeDropdown); + return ( @@ -107,6 +124,7 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} + onClick={openDropdown} > {button} @@ -122,6 +140,7 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} + onClick={openDropdown} > {buttonVariant === "border-with-text" ? ( = observer((props) => { )} - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) + {isOpen && ( + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) ) : ( -

No matching results

- ) - ) : ( -

Loading...

- )} +

Loading...

+ )} +
-
- + + )} ); }); diff --git a/web/components/dropdowns/member/types.d.ts b/web/components/dropdowns/member/types.d.ts index 4c0bff67b..f5f81a5c6 100644 --- a/web/components/dropdowns/member/types.d.ts +++ b/web/components/dropdowns/member/types.d.ts @@ -11,6 +11,7 @@ export type MemberDropdownProps = { dropdownArrow?: boolean; placeholder?: string; placement?: Placement; + tabIndex?: number; } & ( | { multiple: false; diff --git a/web/components/dropdowns/module.tsx b/web/components/dropdowns/module.tsx index ff35c26b6..e0d6b52f7 100644 --- a/web/components/dropdowns/module.tsx +++ b/web/components/dropdowns/module.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactNode, useEffect, useState } from "react"; +import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; @@ -6,6 +6,8 @@ import { Placement } from "@popperjs/core"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks import { useApplication, useModule } from "hooks/store"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // icons import { DiceIcon } from "@plane/ui"; // helpers @@ -26,6 +28,7 @@ type Props = { placement?: Placement; projectId: string; value: string | null; + tabIndex?: number; }; type DropdownOptions = @@ -104,9 +107,13 @@ export const ModuleDropdown: React.FC = observer((props) => { placement, projectId, value, + tabIndex, } = props; // states const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + // refs + const dropdownRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -166,15 +173,26 @@ export const ModuleDropdown: React.FC = observer((props) => { const selectedModule = value ? getModuleById(value) : null; + const openDropdown = () => { + setIsOpen(true); + if (referenceElement) referenceElement.focus(); + }; + const closeDropdown = () => setIsOpen(false); + const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); + useOutsideClickDetector(dropdownRef, closeDropdown); + return ( {button ? ( @@ -182,6 +200,7 @@ export const ModuleDropdown: React.FC = observer((props) => { ref={setReferenceElement} type="button" className={cn("block h-full w-full outline-none", buttonContainerClassName)} + onClick={openDropdown} > {button} @@ -197,6 +216,7 @@ export const ModuleDropdown: React.FC = observer((props) => { }, buttonContainerClassName )} + onClick={openDropdown} > {buttonVariant === "border-with-text" ? ( = observer((props) => { )} - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) + {isOpen && ( + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) ) : ( -

No matching results

- ) - ) : ( -

Loading...

- )} +

Loading...

+ )} +
-
- + + )} ); }); diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index 5c467a7b6..bd20c7965 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -1,8 +1,11 @@ -import { Fragment, ReactNode, useState } from "react"; +import { Fragment, ReactNode, useRef, useState } from "react"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; import { Placement } from "@popperjs/core"; import { Check, ChevronDown, Search } from "lucide-react"; +// hooks +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // icons import { PriorityIcon } from "@plane/ui"; // helpers @@ -25,6 +28,7 @@ type Props = { onChange: (val: TIssuePriorities) => void; placement?: Placement; value: TIssuePriorities; + tabIndex?: number; }; type ButtonProps = { @@ -210,9 +214,13 @@ export const PriorityDropdown: React.FC = (props) => { onChange, placement, value, + tabIndex, } = props; // states const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + // refs + const dropdownRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -250,7 +258,7 @@ export const PriorityDropdown: React.FC = (props) => { >
); diff --git a/web/components/issues/attachment/attachment-upload.tsx b/web/components/issues/attachment/attachment-upload.tsx index c53574cb4..264f0c643 100644 --- a/web/components/issues/attachment/attachment-upload.tsx +++ b/web/components/issues/attachment/attachment-upload.tsx @@ -11,15 +11,15 @@ import { TAttachmentOperations } from "./root"; type TAttachmentOperationsModal = Exclude; type Props = { + workspaceSlug: string; disabled?: boolean; handleAttachmentOperations: TAttachmentOperationsModal; }; export const IssueAttachmentUpload: React.FC = observer((props) => { - const { disabled = false, handleAttachmentOperations } = props; + const { workspaceSlug, disabled = false, handleAttachmentOperations } = props; // store hooks const { - router: { workspaceSlug }, config: { envConfig }, } = useApplication(); // states diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx index 6644d7e8c..2129a4f61 100644 --- a/web/components/issues/attachment/attachments-list.tsx +++ b/web/components/issues/attachment/attachments-list.tsx @@ -7,25 +7,35 @@ import { IssueAttachmentsDetail } from "./attachment-detail"; // types import { TAttachmentOperations } from "./root"; -export type TAttachmentOperationsRemoveModal = Exclude; +type TAttachmentOperationsRemoveModal = Exclude; -export type TIssueAttachmentsList = { +type TIssueAttachmentsList = { + issueId: string; handleAttachmentOperations: TAttachmentOperationsRemoveModal; + disabled?: boolean; }; export const IssueAttachmentsList: FC = observer((props) => { - const { handleAttachmentOperations } = props; + const { issueId, handleAttachmentOperations, disabled } = props; // store hooks const { - attachment: { issueAttachments }, + attachment: { getAttachmentsByIssueId }, } = useIssueDetail(); + const issueAttachments = getAttachmentsByIssueId(issueId); + + if (!issueAttachments) return <>; + return ( <> {issueAttachments && issueAttachments.length > 0 && issueAttachments.map((attachmentId) => ( - + ))} ); diff --git a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx index 6c26bf850..e01d2828e 100644 --- a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx +++ b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx @@ -8,12 +8,15 @@ import { Button } from "@plane/ui"; import { getFileName } from "helpers/attachment.helper"; // types import type { TIssueAttachment } from "@plane/types"; -import { TIssueAttachmentsList } from "./attachments-list"; +import { TAttachmentOperations } from "./root"; -type Props = TIssueAttachmentsList & { +export type TAttachmentOperationsRemoveModal = Exclude; + +type Props = { isOpen: boolean; setIsOpen: Dispatch>; data: TIssueAttachment; + handleAttachmentOperations: TAttachmentOperationsRemoveModal; }; export const IssueAttachmentDeleteModal: FC = (props) => { diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx index 9d8a31b05..79a6dc840 100644 --- a/web/components/issues/attachment/root.tsx +++ b/web/components/issues/attachment/root.tsx @@ -1,13 +1,16 @@ import { FC, useMemo } from "react"; // hooks -import { useApplication, useIssueDetail } from "hooks/store"; +import { useIssueDetail } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { IssueAttachmentUpload } from "./attachment-upload"; import { IssueAttachmentsList } from "./attachments-list"; export type TIssueAttachmentRoot = { - isEditable: boolean; + workspaceSlug: string; + projectId: string; + issueId: string; + disabled?: boolean; }; export type TAttachmentOperations = { @@ -17,12 +20,9 @@ export type TAttachmentOperations = { export const IssueAttachmentRoot: FC = (props) => { // props - const { isEditable } = props; + const { workspaceSlug, projectId, issueId, disabled = false } = props; // hooks - const { - router: { workspaceSlug, projectId }, - } = useApplication(); - const { issueId, createAttachment, removeAttachment } = useIssueDetail(); + const { createAttachment, removeAttachment } = useIssueDetail(); const { setToastAlert } = useToast(); const handleAttachmentOperations: TAttachmentOperations = useMemo( @@ -69,8 +69,16 @@ export const IssueAttachmentRoot: FC = (props) => {

Attachments

- - + +
); diff --git a/web/components/issues/comment/comment-reaction.tsx b/web/components/issues/comment/comment-reaction.tsx index eb80b0323..a59337575 100644 --- a/web/components/issues/comment/comment-reaction.tsx +++ b/web/components/issues/comment/comment-reaction.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite"; import { useUser } from "hooks/store"; import useCommentReaction from "hooks/use-comment-reaction"; // ui -import { ReactionSelector } from "components/core"; +// import { ReactionSelector } from "components/core"; // helper import { renderEmoji } from "helpers/emoji.helper"; // types @@ -47,7 +47,8 @@ export const CommentReaction: FC = observer((props) => { return (
- {!readonly && ( + {/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */} + {/* {!readonly && ( = observer((props) => { } onSelect={handleReactionClick} /> - )} + )} */} {Object.keys(groupedReactions || {}).map( (reaction) => diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 3f463496e..f020672d2 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -5,9 +5,10 @@ import useReloadConfirmations from "hooks/use-reload-confirmation"; import debounce from "lodash/debounce"; // components import { TextArea } from "@plane/ui"; -import { RichTextEditor } from "@plane/rich-text-editor"; +import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; // types import { TIssue } from "@plane/types"; +import { TIssueOperations } from "./issue-detail"; // services import { FileService } from "services/file.service"; import { useMention } from "hooks/store"; @@ -18,15 +19,17 @@ export interface IssueDescriptionFormValues { } export interface IssueDetailsProps { + workspaceSlug: string; + projectId: string; + issueId: string; issue: { name: string; description_html: string; id: string; project_id?: string; }; - workspaceSlug: string; - handleFormSubmit: (value: IssueDescriptionFormValues) => Promise; - isAllowed: boolean; + issueOperations: TIssueOperations; + disabled: boolean; isSubmitting: "submitting" | "submitted" | "saved"; setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } @@ -34,7 +37,7 @@ export interface IssueDetailsProps { const fileService = new FileService(); export const IssueDescriptionForm: FC = (props) => { - const { issue, handleFormSubmit, workspaceSlug, isAllowed, isSubmitting, setIsSubmitting } = props; + const { workspaceSlug, projectId, issueId, issue, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; // states const [characterLimit, setCharacterLimit] = useState(false); @@ -75,12 +78,18 @@ export const IssueDescriptionForm: FC = (props) => { async (formData: Partial) => { if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; - await handleFormSubmit({ - name: formData.name ?? "", - description_html: formData.description_html ?? "

", - }); + await issueOperations.update( + workspaceSlug, + projectId, + issueId, + { + name: formData.name ?? "", + description_html: formData.description_html ?? "

", + }, + false + ); }, - [handleFormSubmit] + [workspaceSlug, projectId, issueId, issueOperations] ); useEffect(() => { @@ -116,7 +125,7 @@ export const IssueDescriptionForm: FC = (props) => { return (
- {isAllowed ? ( + {!disabled ? ( = (props) => { className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary" hasError={Boolean(errors?.name)} role="textbox" - disabled={!isAllowed} /> )} /> ) : (

{issue.name}

)} - {characterLimit && isAllowed && ( + {characterLimit && !disabled && (
255 ? "text-red-500" : ""}`}> {watch("name").length} @@ -159,29 +167,37 @@ export const IssueDescriptionForm: FC = (props) => { ( - { - setShowAlert(true); - setIsSubmitting("submitting"); - onChange(description_html); - debouncedFormSave(); - }} - mentionSuggestions={mentionSuggestions} - mentionHighlights={mentionHighlights} - /> - )} + render={({ field: { onChange } }) => + !disabled ? ( + { + setShowAlert(true); + setIsSubmitting("submitting"); + onChange(description_html); + debouncedFormSave(); + }} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} + /> + ) : ( + + ) + } />
diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index b8af27d40..f1c6636cd 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -1,21 +1,22 @@ export * from "./attachment"; export * from "./comment"; export * from "./issue-modal"; -export * from "./sidebar-select"; export * from "./view-select"; export * from "./activity"; export * from "./delete-issue-modal"; export * from "./description-form"; export * from "./issue-layouts"; -export * from "./peek-overview"; -export * from "./main-content"; + export * from "./parent-issues-list-modal"; -export * from "./sidebar"; export * from "./label"; -export * from "./issue-reaction"; export * from "./confirm-issue-discard"; export * from "./issue-update-status"; +// issue details +export * from "./issue-detail"; + +export * from "./peek-overview"; + // draft issue export * from "./draft-issue-form"; export * from "./draft-issue-modal"; @@ -23,6 +24,3 @@ export * from "./delete-draft-issue-modal"; // archived issue export * from "./delete-archived-issue-modal"; - -// issue links -export * from "./issue-links"; diff --git a/web/components/issues/issue-detail/cycle-select.tsx b/web/components/issues/issue-detail/cycle-select.tsx new file mode 100644 index 000000000..24ed1c963 --- /dev/null +++ b/web/components/issues/issue-detail/cycle-select.tsx @@ -0,0 +1,103 @@ +import React, { ReactNode, useState } from "react"; +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; +// hooks +import { useCycle, useIssueDetail } from "hooks/store"; +// ui +import { ContrastIcon, CustomSearchSelect, Spinner, Tooltip } from "@plane/ui"; +// types +import type { TIssueOperations } from "./root"; + +type TIssueCycleSelect = { + workspaceSlug: string; + projectId: string; + issueId: string; + issueOperations: TIssueOperations; + disabled?: boolean; +}; + +export const IssueCycleSelect: React.FC = observer((props) => { + const { workspaceSlug, projectId, issueId, issueOperations, disabled = false } = props; + // hooks + const { getCycleById, currentProjectIncompleteCycleIds, fetchAllCycles } = useCycle(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + // state + const [isUpdating, setIsUpdating] = useState(false); + + useSWR(workspaceSlug && projectId ? `PROJECT_${projectId}_ISSUE_${issueId}_CYCLES` : null, async () => { + if (workspaceSlug && projectId) await fetchAllCycles(workspaceSlug, projectId); + }); + + const issue = getIssueById(issueId); + const projectCycleIds = currentProjectIncompleteCycleIds; + const issueCycle = (issue && issue.cycle_id && getCycleById(issue.cycle_id)) || undefined; + const disableSelect = disabled || isUpdating; + + const handleIssueCycleChange = async (cycleId: string) => { + if (!cycleId) return; + setIsUpdating(true); + if (issue && issue.cycle_id === cycleId) + await issueOperations.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); + else await issueOperations.addIssueToCycle(workspaceSlug, projectId, cycleId, [issueId]); + setIsUpdating(false); + }; + + type TDropdownOptions = { value: string; query: string; content: ReactNode }[]; + const options: TDropdownOptions | undefined = projectCycleIds + ? (projectCycleIds + .map((cycleId) => { + const cycle = getCycleById(cycleId) || undefined; + if (!cycle) return undefined; + return { + value: cycle.id, + query: cycle.name, + content: ( +
+ + + + {cycle.name} +
+ ) as ReactNode, + }; + }) + .filter((cycle) => cycle !== undefined) as TDropdownOptions) + : undefined; + + return ( +
+ handleIssueCycleChange(value)} + options={options} + customButton={ +
+ + + +
+ } + width="max-w-[10rem]" + noChevron + disabled={disableSelect} + /> + {isUpdating && } +
+ ); +}); diff --git a/web/components/issues/issue-detail/index.ts b/web/components/issues/issue-detail/index.ts new file mode 100644 index 000000000..63ef560a1 --- /dev/null +++ b/web/components/issues/issue-detail/index.ts @@ -0,0 +1,14 @@ +export * from "./root"; + +export * from "./main-content"; +export * from "./sidebar"; + +// select +export * from "./cycle-select"; +export * from "./module-select"; +export * from "./parent-select"; +export * from "./relation-select"; +export * from "./parent"; +export * from "./label"; +export * from "./subscription"; +export * from "./links"; diff --git a/web/components/issues/issue-detail/label/create-label.tsx b/web/components/issues/issue-detail/label/create-label.tsx new file mode 100644 index 000000000..7babaee00 --- /dev/null +++ b/web/components/issues/issue-detail/label/create-label.tsx @@ -0,0 +1,159 @@ +import { FC, useState, Fragment, useEffect } from "react"; +import { Plus, X } from "lucide-react"; +import { Controller, useForm } from "react-hook-form"; +import { TwitterPicker } from "react-color"; +import { Popover, Transition } from "@headlessui/react"; +// hooks +import { useIssueDetail } from "hooks/store"; +import useToast from "hooks/use-toast"; +// ui +import { Input } from "@plane/ui"; +// types +import { TLabelOperations } from "./root"; +import { IIssueLabel } from "@plane/types"; + +type ILabelCreate = { + workspaceSlug: string; + projectId: string; + issueId: string; + labelOperations: TLabelOperations; + disabled?: boolean; +}; + +const defaultValues: Partial = { + name: "", + color: "#ff0000", +}; + +export const LabelCreate: FC = (props) => { + const { workspaceSlug, projectId, issueId, labelOperations, disabled = false } = props; + // hooks + const { setToastAlert } = useToast(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + // state + const [isCreateToggle, setIsCreateToggle] = useState(false); + const handleIsCreateToggle = () => setIsCreateToggle(!isCreateToggle); + // react hook form + const { + handleSubmit, + formState: { errors, isSubmitting }, + reset, + control, + setFocus, + } = useForm>({ + defaultValues, + }); + + useEffect(() => { + if (!isCreateToggle) return; + + setFocus("name"); + reset(); + }, [isCreateToggle, reset, setFocus]); + + const handleLabel = async (formData: Partial) => { + if (!workspaceSlug || !projectId || isSubmitting) return; + + try { + const issue = getIssueById(issueId); + const labelResponse = await labelOperations.createLabel(workspaceSlug, projectId, formData); + const currentLabels = [...(issue?.label_ids || []), labelResponse.id]; + await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels }); + reset(defaultValues); + } catch (error) { + setToastAlert({ + title: "Label creation failed", + type: "error", + message: "Label creation failed. Please try again sometime later.", + }); + } + }; + + return ( + <> +
+
+ {isCreateToggle ? : } +
+
{isCreateToggle ? "Cancel" : "New"}
+
+ + {isCreateToggle && ( +
+
+ ( + + <> + + {value && value?.trim() !== "" && ( + + )} + + + + + onChange(value.hex)} /> + + + + + )} + /> +
+ ( + + )} + /> + + + + )} + + ); +}; diff --git a/web/components/issues/issue-detail/label/index.ts b/web/components/issues/issue-detail/label/index.ts new file mode 100644 index 000000000..83f1e73bc --- /dev/null +++ b/web/components/issues/issue-detail/label/index.ts @@ -0,0 +1,7 @@ +export * from "./root"; + +export * from "./label-list"; +export * from "./label-list-item"; +export * from "./create-label"; +export * from "./select/root"; +export * from "./select/label-select"; diff --git a/web/components/issues/issue-detail/label/label-list-item.tsx b/web/components/issues/issue-detail/label/label-list-item.tsx new file mode 100644 index 000000000..3c3164c5a --- /dev/null +++ b/web/components/issues/issue-detail/label/label-list-item.tsx @@ -0,0 +1,57 @@ +import { FC } from "react"; +import { X } from "lucide-react"; +// types +import { TLabelOperations } from "./root"; +import { useIssueDetail, useLabel } from "hooks/store"; + +type TLabelListItem = { + workspaceSlug: string; + projectId: string; + issueId: string; + labelId: string; + labelOperations: TLabelOperations; + disabled: boolean; +}; + +export const LabelListItem: FC = (props) => { + const { workspaceSlug, projectId, issueId, labelId, labelOperations, disabled } = props; + // hooks + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getLabelById } = useLabel(); + + const issue = getIssueById(issueId); + const label = getLabelById(labelId); + + const handleLabel = async () => { + if (issue && !disabled) { + const currentLabels = issue.label_ids.filter((_labelId) => _labelId !== labelId); + await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels }); + } + }; + + if (!label) return <>; + return ( +
+
+
{label.name}
+ {!disabled && ( +
+ +
+ )} +
+ ); +}; diff --git a/web/components/issues/issue-detail/label/label-list.tsx b/web/components/issues/issue-detail/label/label-list.tsx new file mode 100644 index 000000000..fd714e002 --- /dev/null +++ b/web/components/issues/issue-detail/label/label-list.tsx @@ -0,0 +1,42 @@ +import { FC } from "react"; +// components +import { LabelListItem } from "./label-list-item"; +// hooks +import { useIssueDetail } from "hooks/store"; +// types +import { TLabelOperations } from "./root"; + +type TLabelList = { + workspaceSlug: string; + projectId: string; + issueId: string; + labelOperations: TLabelOperations; + disabled: boolean; +}; + +export const LabelList: FC = (props) => { + const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props; + // hooks + const { + issue: { getIssueById }, + } = useIssueDetail(); + + const issue = getIssueById(issueId); + const issueLabels = issue?.label_ids || undefined; + + if (!issue || !issueLabels) return <>; + return ( + <> + {issueLabels.map((labelId) => ( + + ))} + + ); +}; diff --git a/web/components/issues/issue-detail/label/root.tsx b/web/components/issues/issue-detail/label/root.tsx new file mode 100644 index 000000000..93e303f61 --- /dev/null +++ b/web/components/issues/issue-detail/label/root.tsx @@ -0,0 +1,101 @@ +import { FC, useMemo } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./"; +// hooks +import { useIssueDetail, useLabel } from "hooks/store"; +// types +import { IIssueLabel, TIssue } from "@plane/types"; +import useToast from "hooks/use-toast"; + +export type TIssueLabel = { + workspaceSlug: string; + projectId: string; + issueId: string; + disabled: boolean; +}; + +export type TLabelOperations = { + updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + createLabel: (workspaceSlug: string, projectId: string, data: Partial) => Promise; +}; + +export const IssueLabel: FC = observer((props) => { + const { workspaceSlug, projectId, issueId, disabled = false } = props; + // hooks + const { updateIssue } = useIssueDetail(); + const { + project: { createLabel }, + } = useLabel(); + const { setToastAlert } = useToast(); + + const labelOperations: TLabelOperations = useMemo( + () => ({ + updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + try { + await updateIssue(workspaceSlug, projectId, issueId, data); + setToastAlert({ + title: "Issue updated successfully", + type: "success", + message: "Issue updated successfully", + }); + } catch (error) { + setToastAlert({ + title: "Issue update failed", + type: "error", + message: "Issue update failed", + }); + } + }, + createLabel: async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const labelResponse = await createLabel(workspaceSlug, projectId, data); + setToastAlert({ + title: "Label created successfully", + type: "success", + message: "Label created successfully", + }); + return labelResponse; + } catch (error) { + setToastAlert({ + title: "Label creation failed", + type: "error", + message: "Label creation failed", + }); + return error; + } + }, + }), + [updateIssue, createLabel, setToastAlert] + ); + + return ( +
+ + + {!disabled && ( + + )} + + {!disabled && ( + + )} +
+ ); +}); diff --git a/web/components/issues/issue-detail/label/select/label-select.tsx b/web/components/issues/issue-detail/label/select/label-select.tsx new file mode 100644 index 000000000..c553ef333 --- /dev/null +++ b/web/components/issues/issue-detail/label/select/label-select.tsx @@ -0,0 +1,159 @@ +import { Fragment, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { usePopper } from "react-popper"; +import { Check, Search, Tag } from "lucide-react"; +// hooks +import { useIssueDetail, useLabel } from "hooks/store"; +// components +import { Combobox } from "@headlessui/react"; + +export interface IIssueLabelSelect { + workspaceSlug: string; + projectId: string; + issueId: string; + onSelect: (_labelIds: string[]) => void; +} + +export const IssueLabelSelect: React.FC = observer((props) => { + const { workspaceSlug, projectId, issueId, onSelect } = props; + // store hooks + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { + project: { fetchProjectLabels, projectLabels }, + } = useLabel(); + // states + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [query, setQuery] = useState(""); + + const issue = getIssueById(issueId); + + const fetchLabels = () => { + setIsLoading(true); + if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false)); + }; + + const options = (projectLabels ?? []).map((label) => ({ + value: label.id, + query: label.name, + content: ( +
+ +
{label.name}
+
+ ), + })); + + const filteredOptions = + query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const issueLabels = issue?.label_ids ?? []; + + const label = ( +
+
+ +
+
Select Label
+
+ ); + + if (!issue) return <>; + + return ( + <> + onSelect(value)} + multiple + > + + + + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {isLoading ? ( +

Loading...

+ ) : filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 hover:bg-custom-background-80 ${ + selected ? "text-custom-text-100" : "text-custom-text-200" + }` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && ( +
+ +
+ )} + + )} +
+ )) + ) : ( + +

No matching results

+
+ )} +
+
+
+
+ + ); +}); diff --git a/web/components/issues/issue-detail/label/select/root.tsx b/web/components/issues/issue-detail/label/select/root.tsx new file mode 100644 index 000000000..c31e1bc61 --- /dev/null +++ b/web/components/issues/issue-detail/label/select/root.tsx @@ -0,0 +1,24 @@ +import { FC } from "react"; +// components +import { IssueLabelSelect } from "./label-select"; +// types +import { TLabelOperations } from "../root"; + +type TIssueLabelSelectRoot = { + workspaceSlug: string; + projectId: string; + issueId: string; + labelOperations: TLabelOperations; +}; + +export const IssueLabelSelectRoot: FC = (props) => { + const { workspaceSlug, projectId, issueId, labelOperations } = props; + + const handleLabel = async (_labelIds: string[]) => { + await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: _labelIds }); + }; + + return ( + + ); +}; diff --git a/web/components/issues/issue-links/create-update-link-modal.tsx b/web/components/issues/issue-detail/links/create-update-link-modal.tsx similarity index 99% rename from web/components/issues/issue-links/create-update-link-modal.tsx rename to web/components/issues/issue-detail/links/create-update-link-modal.tsx index 1cbcd4656..fc9eb3838 100644 --- a/web/components/issues/issue-links/create-update-link-modal.tsx +++ b/web/components/issues/issue-detail/links/create-update-link-modal.tsx @@ -42,7 +42,7 @@ export const IssueLinkCreateUpdateModal: FC = (props) const onClose = () => { handleModal(false); const timeout = setTimeout(() => { - reset(defaultValues); + reset(preloadedData ? preloadedData : defaultValues); clearTimeout(timeout); }, 500); }; diff --git a/web/components/issues/issue-detail/links/index.ts b/web/components/issues/issue-detail/links/index.ts new file mode 100644 index 000000000..4a06c89af --- /dev/null +++ b/web/components/issues/issue-detail/links/index.ts @@ -0,0 +1,4 @@ +export * from "./root"; + +export * from "./links"; +export * from "./link-detail"; diff --git a/web/components/issues/issue-links/link-detail.tsx b/web/components/issues/issue-detail/links/link-detail.tsx similarity index 81% rename from web/components/issues/issue-links/link-detail.tsx rename to web/components/issues/issue-detail/links/link-detail.tsx index d00e43597..c92c13977 100644 --- a/web/components/issues/issue-links/link-detail.tsx +++ b/web/components/issues/issue-detail/links/link-detail.tsx @@ -1,5 +1,6 @@ import { FC, useState } from "react"; // hooks +import useToast from "hooks/use-toast"; import { useIssueDetail } from "hooks/store"; // ui import { ExternalLinkIcon, Tooltip } from "@plane/ui"; @@ -9,6 +10,7 @@ import { Pencil, Trash2, LinkIcon } from "lucide-react"; import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; export type TIssueLinkDetail = { linkId: string; @@ -21,11 +23,17 @@ export const IssueLinkDetail: FC = (props) => { const { linkId, linkOperations, isNotAllowed } = props; // hooks const { + toggleIssueLinkModal: toggleIssueLinkModalStore, link: { getLinkById }, } = useIssueDetail(); + const { setToastAlert } = useToast(); + // state const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false); - const toggleIssueLinkModal = (modalToggle: boolean) => setIsIssueLinkModalOpen(modalToggle); + const toggleIssueLinkModal = (modalToggle: boolean) => { + toggleIssueLinkModalStore(modalToggle); + setIsIssueLinkModalOpen(modalToggle); + }; const linkDetail = getLinkById(linkId); if (!linkDetail) return <>; @@ -40,18 +48,23 @@ export const IssueLinkDetail: FC = (props) => { />
-
+
{ + copyTextToClipboard(linkDetail.url); + setToastAlert({ + type: "success", + title: "Link copied!", + message: "Link copied to clipboard", + }); + }} + >
- - // copyToClipboard(linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url) - // } - > + {linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url} @@ -65,7 +78,7 @@ export const IssueLinkDetail: FC = (props) => { onClick={(e) => { e.preventDefault(); e.stopPropagation(); - setIsIssueLinkModalOpen(true); + toggleIssueLinkModal(true); }} > diff --git a/web/components/issues/issue-links/links.tsx b/web/components/issues/issue-detail/links/links.tsx similarity index 83% rename from web/components/issues/issue-links/links.tsx rename to web/components/issues/issue-detail/links/links.tsx index dbcb411ce..368bddb91 100644 --- a/web/components/issues/issue-links/links.tsx +++ b/web/components/issues/issue-detail/links/links.tsx @@ -9,20 +9,25 @@ import { TLinkOperations } from "./root"; export type TLinkOperationsModal = Exclude; export type TIssueLinkList = { + issueId: string; linkOperations: TLinkOperationsModal; }; export const IssueLinkList: FC = observer((props) => { // props - const { linkOperations } = props; + const { issueId, linkOperations } = props; // hooks const { - link: { issueLinks }, + link: { getLinksByIssueId }, } = useIssueDetail(); const { membership: { currentProjectRole }, } = useUser(); + const issueLinks = getLinksByIssueId(issueId); + + if (!issueLinks) return <>; + return (
{issueLinks && diff --git a/web/components/issues/issue-links/root.tsx b/web/components/issues/issue-detail/links/root.tsx similarity index 76% rename from web/components/issues/issue-links/root.tsx rename to web/components/issues/issue-detail/links/root.tsx index bd2db3d39..94124085a 100644 --- a/web/components/issues/issue-links/root.tsx +++ b/web/components/issues/issue-detail/links/root.tsx @@ -1,7 +1,7 @@ -import { FC, useMemo, useState } from "react"; +import { FC, useCallback, useMemo, useState } from "react"; import { Plus } from "lucide-react"; // hooks -import { useApplication, useIssueDetail } from "hooks/store"; +import { useIssueDetail } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { IssueLinkCreateUpdateModal } from "./create-update-link-modal"; @@ -16,21 +16,26 @@ export type TLinkOperations = { }; export type TIssueLinkRoot = { - uneditable: boolean; - isAllowed: boolean; + workspaceSlug: string; + projectId: string; + issueId: string; + disabled?: boolean; }; export const IssueLinkRoot: FC = (props) => { // props - const { uneditable, isAllowed } = props; + const { workspaceSlug, projectId, issueId, disabled = false } = props; // hooks - const { - router: { workspaceSlug, projectId }, - } = useApplication(); - const { issueId, createLink, updateLink, removeLink } = useIssueDetail(); + const { toggleIssueLinkModal: toggleIssueLinkModalStore, createLink, updateLink, removeLink } = useIssueDetail(); // state - const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false); - const toggleIssueLinkModal = (modalToggle: boolean) => setIsIssueLinkModalOpen(modalToggle); + const [isIssueLinkModal, setIsIssueLinkModal] = useState(false); + const toggleIssueLinkModal = useCallback( + (modalToggle: boolean) => { + toggleIssueLinkModalStore(modalToggle); + setIsIssueLinkModal(modalToggle); + }, + [toggleIssueLinkModalStore] + ); const { setToastAlert } = useToast(); @@ -91,28 +96,28 @@ export const IssueLinkRoot: FC = (props) => { } }, }), - [workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, setToastAlert] + [workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, setToastAlert, toggleIssueLinkModal] ); return ( <> -
+

Links

- {isAllowed && ( + {!disabled && ( @@ -120,7 +125,7 @@ export const IssueLinkRoot: FC = (props) => {
- +
diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx new file mode 100644 index 000000000..fcbe54a1c --- /dev/null +++ b/web/components/issues/issue-detail/main-content.tsx @@ -0,0 +1,121 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; +// hooks +import { useIssueDetail, useProject, useProjectState, useUser } from "hooks/store"; +// components +import { IssueDescriptionForm, IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; +import { IssueParentDetail } from "./parent"; +import { IssueReaction } from "./reactions"; +import { SubIssuesRoot } from "../sub-issues"; +// ui +import { StateGroupIcon } from "@plane/ui"; +// types +import { TIssueOperations } from "./root"; + +type Props = { + workspaceSlug: string; + projectId: string; + issueId: string; + issueOperations: TIssueOperations; + is_archived: boolean; + is_editable: boolean; +}; + +export const IssueMainContent: React.FC = observer((props) => { + const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props; + // states + const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); + // hooks + const { currentUser } = useUser(); + const { getProjectById } = useProject(); + const { projectStates } = useProjectState(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + + const issue = getIssueById(issueId); + if (!issue) return <>; + + const projectDetails = projectId ? getProjectById(projectId) : null; + const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); + + return ( + <> +
+ {issue.parent_id && ( + + )} + +
+ {currentIssueState && ( + + )} + +
+ + setIsSubmitting(value)} + isSubmitting={isSubmitting} + issue={issue} + issueOperations={issueOperations} + disabled={!is_editable} + /> + + {currentUser && ( + + )} + + {currentUser && ( + + )} +
+ + {/* issue attachments */} + + + {/*
+

Comments/Activity

+ + +
*/} + + ); +}); diff --git a/web/components/issues/issue-detail/module-select.tsx b/web/components/issues/issue-detail/module-select.tsx new file mode 100644 index 000000000..4ac5f1fa5 --- /dev/null +++ b/web/components/issues/issue-detail/module-select.tsx @@ -0,0 +1,103 @@ +import React, { ReactNode, useState } from "react"; +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; +// hooks +import { useModule, useIssueDetail } from "hooks/store"; +// ui +import { CustomSearchSelect, DiceIcon, Spinner, Tooltip } from "@plane/ui"; +// types +import type { TIssueOperations } from "./root"; + +type TIssueModuleSelect = { + workspaceSlug: string; + projectId: string; + issueId: string; + issueOperations: TIssueOperations; + disabled?: boolean; +}; + +export const IssueModuleSelect: React.FC = observer((props) => { + const { workspaceSlug, projectId, issueId, issueOperations, disabled = false } = props; + // hooks + const { getModuleById, projectModuleIds, fetchModules } = useModule(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + // state + const [isUpdating, setIsUpdating] = useState(false); + + useSWR(workspaceSlug && projectId ? `PROJECT_${projectId}_ISSUE_${issueId}_MODULES` : null, async () => { + if (workspaceSlug && projectId) await fetchModules(workspaceSlug, projectId); + }); + + const issue = getIssueById(issueId); + const issueModule = (issue && issue.module_id && getModuleById(issue.module_id)) || undefined; + const disableSelect = disabled || isUpdating; + + const handleIssueModuleChange = async (moduleId: string) => { + if (!moduleId) return; + setIsUpdating(true); + if (issue && issue.module_id === moduleId) + await issueOperations.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); + else await issueOperations.addIssueToModule(workspaceSlug, projectId, moduleId, [issueId]); + setIsUpdating(false); + }; + + type TDropdownOptions = { value: string; query: string; content: ReactNode }[]; + const options: TDropdownOptions | undefined = projectModuleIds + ? (projectModuleIds + .map((moduleId) => { + const _module = getModuleById(moduleId); + if (!_module) return undefined; + + return { + value: _module.id, + query: _module.name, + content: ( +
+ + + + {_module.name} +
+ ) as ReactNode, + }; + }) + .filter((_module) => _module !== undefined) as TDropdownOptions) + : undefined; + + return ( +
+ handleIssueModuleChange(value)} + options={options} + customButton={ +
+ + + +
+ } + width="max-w-[10rem]" + noChevron + disabled={disableSelect} + /> + {isUpdating && } +
+ ); +}); diff --git a/web/components/issues/issue-detail/parent-select.tsx b/web/components/issues/issue-detail/parent-select.tsx new file mode 100644 index 000000000..2a7fb3d83 --- /dev/null +++ b/web/components/issues/issue-detail/parent-select.tsx @@ -0,0 +1,86 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +// hooks +import { useIssueDetail, useProject } from "hooks/store"; +import { Spinner } from "@plane/ui"; +// components +import { ParentIssuesListModal } from "components/issues"; +import { TIssueOperations } from "./root"; + +type TIssueParentSelect = { + workspaceSlug: string; + projectId: string; + issueId: string; + issueOperations: TIssueOperations; + + disabled?: boolean; +}; + +export const IssueParentSelect: React.FC = observer( + ({ workspaceSlug, projectId, issueId, issueOperations, disabled = false }) => { + // hooks + const { getProjectById } = useProject(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + // state + const { isParentIssueModalOpen, toggleParentIssueModal } = useIssueDetail(); + const [updating, setUpdating] = useState(false); + + const issue = getIssueById(issueId); + + const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined; + const parentIssueProjectDetails = + parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined; + + const handleParentIssue = async (_issueId: string | null = null) => { + setUpdating(true); + try { + await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId }); + await issueOperations.fetch(workspaceSlug, projectId, issueId); + toggleParentIssueModal(false); + setUpdating(false); + } catch (error) { + console.error("something went wrong while fetching the issue"); + } + }; + + if (!issue) return <>; + + return ( +
+ toggleParentIssueModal(false)} + onChange={(issue: any) => handleParentIssue(issue?.id)} + /> + + + + {updating && } +
+ ); + } +); diff --git a/web/components/issues/issue-detail/parent/index.ts b/web/components/issues/issue-detail/parent/index.ts new file mode 100644 index 000000000..1b5a96749 --- /dev/null +++ b/web/components/issues/issue-detail/parent/index.ts @@ -0,0 +1,4 @@ +export * from "./root"; + +export * from "./siblings"; +export * from "./sibling-item"; diff --git a/web/components/issues/issue-detail/parent/root.tsx b/web/components/issues/issue-detail/parent/root.tsx new file mode 100644 index 000000000..2176ccecc --- /dev/null +++ b/web/components/issues/issue-detail/parent/root.tsx @@ -0,0 +1,72 @@ +import { FC } from "react"; +import Link from "next/link"; +import { MinusCircle } from "lucide-react"; +// component +import { IssueParentSiblings } from "./siblings"; +// ui +import { CustomMenu } from "@plane/ui"; +// hooks +import { useIssueDetail, useIssues, useProject, useProjectState } from "hooks/store"; +// types +import { TIssueOperations } from "../root"; +import { TIssue } from "@plane/types"; + +export type TIssueParentDetail = { + workspaceSlug: string; + projectId: string; + issueId: string; + issue: TIssue; + issueOperations: TIssueOperations; +}; + +export const IssueParentDetail: FC = (props) => { + const { workspaceSlug, projectId, issueId, issue, issueOperations } = props; + // hooks + const { issueMap } = useIssues(); + const { peekIssue } = useIssueDetail(); + const { getProjectById } = useProject(); + const { getProjectStates } = useProjectState(); + + const parentIssue = issueMap?.[issue.parent_id || ""] || undefined; + + const issueParentState = getProjectStates(parentIssue?.project_id)?.find( + (state) => state?.id === parentIssue?.state_id + ); + const stateColor = issueParentState?.color || undefined; + + if (!parentIssue) return <>; + + return ( + <> +
+ +
+
+ + + {getProjectById(parentIssue.project_id)?.identifier}-{parentIssue?.sequence_id} + +
+ {(parentIssue?.name ?? "").substring(0, 50)} +
+ + + +
+ Sibling issues +
+ + + + issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null })} + className="flex items-center gap-2 py-2 text-red-500" + > + + Remove Parent Issue + +
+
+ + ); +}; diff --git a/web/components/issues/issue-detail/parent/sibling-item.tsx b/web/components/issues/issue-detail/parent/sibling-item.tsx new file mode 100644 index 000000000..cbcf4741b --- /dev/null +++ b/web/components/issues/issue-detail/parent/sibling-item.tsx @@ -0,0 +1,39 @@ +import { FC } from "react"; +import Link from "next/link"; +// ui +import { CustomMenu, LayersIcon } from "@plane/ui"; +// hooks +import { useIssueDetail, useProject } from "hooks/store"; + +type TIssueParentSiblingItem = { + issueId: string; +}; + +export const IssueParentSiblingItem: FC = (props) => { + const { issueId } = props; + // hooks + const { getProjectById } = useProject(); + const { + peekIssue, + issue: { getIssueById }, + } = useIssueDetail(); + + const issueDetail = (issueId && getIssueById(issueId)) || undefined; + if (!issueDetail) return <>; + + const projectDetails = (issueDetail.project_id && getProjectById(issueDetail.project_id)) || undefined; + + return ( + <> + + + + {projectDetails?.identifier}-{issueDetail.sequence_id} + + + + ); +}; diff --git a/web/components/issues/issue-detail/parent/siblings.tsx b/web/components/issues/issue-detail/parent/siblings.tsx new file mode 100644 index 000000000..bc93ff138 --- /dev/null +++ b/web/components/issues/issue-detail/parent/siblings.tsx @@ -0,0 +1,51 @@ +import { FC } from "react"; +import useSWR from "swr"; +import { observer } from "mobx-react-lite"; +// components +import { IssueParentSiblingItem } from "./sibling-item"; +// hooks +import { useIssueDetail } from "hooks/store"; +// types +import { TIssue } from "@plane/types"; + +export type TIssueParentSiblings = { + currentIssue: TIssue; + parentIssue: TIssue; +}; + +export const IssueParentSiblings: FC = (props) => { + const { currentIssue, parentIssue } = props; + // hooks + const { + peekIssue, + fetchSubIssues, + subIssues: { subIssuesByIssueId }, + } = useIssueDetail(); + + const { isLoading } = useSWR( + peekIssue && parentIssue && parentIssue.project_id + ? `ISSUE_PARENT_CHILD_ISSUES_${peekIssue?.workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}` + : null, + peekIssue && parentIssue && parentIssue.project_id + ? () => fetchSubIssues(peekIssue?.workspaceSlug, parentIssue.project_id, parentIssue.id) + : null + ); + + const subIssueIds = (parentIssue && subIssuesByIssueId(parentIssue.id)) || undefined; + + return ( +
+ {isLoading ? ( +
+ Loading +
+ ) : subIssueIds && subIssueIds.length > 0 ? ( + subIssueIds.map((issueId) => currentIssue.id != issueId && ) + ) : ( +
+ No sibling issues +
+ )} +
+ ); +}; diff --git a/web/components/issues/issue-detail/reactions/index.ts b/web/components/issues/issue-detail/reactions/index.ts new file mode 100644 index 000000000..8dc6f05bd --- /dev/null +++ b/web/components/issues/issue-detail/reactions/index.ts @@ -0,0 +1,4 @@ +export * from "./reaction-selector"; + +export * from "./issue"; +// export * from "./issue-comment"; diff --git a/web/components/issues/issue-detail/reactions/issue.tsx b/web/components/issues/issue-detail/reactions/issue.tsx new file mode 100644 index 000000000..1627a6730 --- /dev/null +++ b/web/components/issues/issue-detail/reactions/issue.tsx @@ -0,0 +1,103 @@ +import { FC, useMemo } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { ReactionSelector } from "./reaction-selector"; +// hooks +import { useIssueDetail } from "hooks/store"; +import useToast from "hooks/use-toast"; +// types +import { IUser } from "@plane/types"; +import { renderEmoji } from "helpers/emoji.helper"; + +export type TIssueReaction = { + workspaceSlug: string; + projectId: string; + issueId: string; + currentUser: IUser; +}; + +export const IssueReaction: FC = observer((props) => { + const { workspaceSlug, projectId, issueId, currentUser } = props; + // hooks + const { + reaction: { getReactionsByIssueId, reactionsByUser }, + createReaction, + removeReaction, + } = useIssueDetail(); + const { setToastAlert } = useToast(); + + const reactionIds = getReactionsByIssueId(issueId); + const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction); + + const issueReactionOperations = useMemo( + () => ({ + create: async (reaction: string) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); + await createReaction(workspaceSlug, projectId, issueId, reaction); + setToastAlert({ + title: "Reaction created successfully", + type: "success", + message: "Reaction created successfully", + }); + } catch (error) { + setToastAlert({ + title: "Reaction creation failed", + type: "error", + message: "Reaction creation failed", + }); + } + }, + remove: async (reaction: string) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); + await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id); + setToastAlert({ + title: "Reaction removed successfully", + type: "success", + message: "Reaction removed successfully", + }); + } catch (error) { + setToastAlert({ + title: "Reaction remove failed", + type: "error", + message: "Reaction remove failed", + }); + } + }, + react: async (reaction: string) => { + if (userReactions.includes(reaction)) await issueReactionOperations.remove(reaction); + else await issueReactionOperations.create(reaction); + }, + }), + [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, setToastAlert, userReactions] + ); + + return ( +
+ + + {reactionIds && + Object.keys(reactionIds || {}).map( + (reaction) => + reactionIds[reaction]?.length > 0 && ( + <> + + + ) + )} +
+ ); +}); diff --git a/web/components/core/reaction-selector.tsx b/web/components/issues/issue-detail/reactions/reaction-selector.tsx similarity index 100% rename from web/components/core/reaction-selector.tsx rename to web/components/issues/issue-detail/reactions/reaction-selector.tsx diff --git a/web/components/issues/sidebar-select/relation.tsx b/web/components/issues/issue-detail/relation-select.tsx similarity index 82% rename from web/components/issues/sidebar-select/relation.tsx rename to web/components/issues/issue-detail/relation-select.tsx index 58e0d720b..30a81f2dd 100644 --- a/web/components/issues/sidebar-select/relation.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { X, CopyPlus } from "lucide-react"; // hooks @@ -37,17 +36,16 @@ const issueRelationObject: Record = { }, }; -type Props = { +type TIssueRelationSelect = { + workspaceSlug: string; + projectId: string; issueId: string; relationKey: TIssueRelationTypes; disabled?: boolean; }; -export const SidebarIssueRelationSelect: React.FC = observer((props) => { - const { issueId, relationKey, disabled = false } = props; - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; +export const IssueRelationSelect: React.FC = observer((props) => { + const { workspaceSlug, projectId, issueId, relationKey, disabled = false } = props; // hooks const { currentUser } = useUser(); const { getProjectById } = useProject(); @@ -128,22 +126,24 @@ export const SidebarIssueRelationSelect: React.FC = observer((props) => { {issueRelationObject[relationKey].icon(10)} {`${projectDetails?.identifier}-${currentIssue?.sequence_id}`} - + {!disabled && ( + + )}
); }) diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx new file mode 100644 index 000000000..4243ba03e --- /dev/null +++ b/web/components/issues/issue-detail/root.tsx @@ -0,0 +1,241 @@ +import { FC, useMemo } from "react"; +import { useRouter } from "next/router"; +// components +import { IssuePeekOverview } from "components/issues"; +import { IssueMainContent } from "./main-content"; +import { IssueDetailsSidebar } from "./sidebar"; +// ui +import { EmptyState } from "components/common"; +// images +import emptyIssue from "public/empty-state/issue.svg"; +// hooks +import { useIssueDetail, useIssues, useUser } from "hooks/store"; +import useToast from "hooks/use-toast"; +// types +import { TIssue } from "@plane/types"; +// constants +import { EUserProjectRoles } from "constants/project"; +import { EIssuesStoreType } from "constants/issue"; + +export type TIssueOperations = { + fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + update: ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + showToast?: boolean + ) => Promise; + remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; + removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; + addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise; + removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise; +}; + +export type TIssueDetailRoot = { + workspaceSlug: string; + projectId: string; + issueId: string; + is_archived?: boolean; +}; + +export const IssueDetailRoot: FC = (props) => { + const { workspaceSlug, projectId, issueId, is_archived = false } = props; + // router + const router = useRouter(); + // hooks + const { + issue: { getIssueById }, + fetchIssue, + updateIssue, + removeIssue, + addIssueToCycle, + removeIssueFromCycle, + addIssueToModule, + removeIssueFromModule, + } = useIssueDetail(); + const { + issues: { removeIssue: removeArchivedIssue }, + } = useIssues(EIssuesStoreType.ARCHIVED); + const { setToastAlert } = useToast(); + const { + membership: { currentProjectRole }, + } = useUser(); + + const issueOperations: TIssueOperations = useMemo( + () => ({ + fetch: async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + await fetchIssue(workspaceSlug, projectId, issueId); + } catch (error) { + console.error("Error fetching the parent issue"); + } + }, + update: async ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + showToast: boolean = true + ) => { + try { + await updateIssue(workspaceSlug, projectId, issueId, data); + if (showToast) { + setToastAlert({ + title: "Issue updated successfully", + type: "success", + message: "Issue updated successfully", + }); + } + } catch (error) { + setToastAlert({ + title: "Issue update failed", + type: "error", + message: "Issue update failed", + }); + } + }, + remove: async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId); + else await removeIssue(workspaceSlug, projectId, issueId); + setToastAlert({ + title: "Issue deleted successfully", + type: "success", + message: "Issue deleted successfully", + }); + } catch (error) { + setToastAlert({ + title: "Issue delete failed", + type: "error", + message: "Issue delete failed", + }); + } + }, + addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { + try { + await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); + setToastAlert({ + title: "Cycle added to issue successfully", + type: "success", + message: "Issue added to issue successfully", + }); + } catch (error) { + setToastAlert({ + title: "Cycle add to issue failed", + type: "error", + message: "Cycle add to issue failed", + }); + } + }, + removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { + try { + await removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); + setToastAlert({ + title: "Cycle removed from issue successfully", + type: "success", + message: "Cycle removed from issue successfully", + }); + } catch (error) { + setToastAlert({ + title: "Cycle remove from issue failed", + type: "error", + message: "Cycle remove from issue failed", + }); + } + }, + addIssueToModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => { + try { + await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds); + setToastAlert({ + title: "Module added to issue successfully", + type: "success", + message: "Module added to issue successfully", + }); + } catch (error) { + setToastAlert({ + title: "Module add to issue failed", + type: "error", + message: "Module add to issue failed", + }); + } + }, + removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { + try { + await removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); + setToastAlert({ + title: "Module removed from issue successfully", + type: "success", + message: "Module removed from issue successfully", + }); + } catch (error) { + setToastAlert({ + title: "Module remove from issue failed", + type: "error", + message: "Module remove from issue failed", + }); + } + }, + }), + [ + is_archived, + fetchIssue, + updateIssue, + removeIssue, + removeArchivedIssue, + addIssueToCycle, + removeIssueFromCycle, + addIssueToModule, + removeIssueFromModule, + setToastAlert, + ] + ); + + // issue details + const issue = getIssueById(issueId); + // checking if issue is editable, based on user role + const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + + return ( + <> + {!issue ? ( + router.push(`/${workspaceSlug}/projects/${projectId}/issues`), + }} + /> + ) : ( +
+
+ +
+
+ +
+
+ )} + + {/* peek overview */} + + + ); +}; diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx new file mode 100644 index 000000000..6b249f4bd --- /dev/null +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -0,0 +1,452 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { CalendarDays, LinkIcon, Signal, Tag, Trash2, Triangle, LayoutPanelTop } from "lucide-react"; +// hooks +import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store"; +import useToast from "hooks/use-toast"; +// components +import { + DeleteIssueModal, + IssueLinkRoot, + IssueRelationSelect, + IssueCycleSelect, + IssueModuleSelect, + IssueParentSelect, + IssueLabel, +} from "components/issues"; +import { IssueSubscription } from "./subscription"; +import { EstimateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns"; +// ui +import { CustomDatePicker } from "components/ui"; +// icons +import { ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; +// helpers +import { copyTextToClipboard } from "helpers/string.helper"; +// types +import type { TIssueOperations } from "./root"; + +type Props = { + workspaceSlug: string; + projectId: string; + issueId: string; + issueOperations: TIssueOperations; + is_archived: boolean; + is_editable: boolean; + fieldsToShow?: ( + | "state" + | "assignee" + | "priority" + | "estimate" + | "parent" + | "blocker" + | "blocked" + | "startDate" + | "dueDate" + | "cycle" + | "module" + | "label" + | "link" + | "delete" + | "all" + | "subscribe" + | "duplicate" + | "relates_to" + )[]; +}; + +export const IssueDetailsSidebar: React.FC = observer((props) => { + const { + workspaceSlug, + projectId, + issueId, + issueOperations, + is_archived, + is_editable, + fieldsToShow = ["all"], + } = props; + // router + const router = useRouter(); + const { inboxIssueId } = router.query; + // store hooks + const { getProjectById } = useProject(); + const { currentUser } = useUser(); + const { projectStates } = useProjectState(); + const { areEstimatesEnabledForCurrentProject } = useEstimate(); + const { setToastAlert } = useToast(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + // states + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + + const issue = getIssueById(issueId); + if (!issue) return <>; + + const handleCopyText = () => { + const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Issue link copied to clipboard.", + }); + }); + }; + + const projectDetails = issue ? getProjectById(issue.project_id) : null; + + const showFirstSection = + fieldsToShow.includes("all") || + fieldsToShow.includes("state") || + fieldsToShow.includes("assignee") || + fieldsToShow.includes("priority") || + fieldsToShow.includes("estimate"); + + const showSecondSection = + fieldsToShow.includes("all") || + fieldsToShow.includes("parent") || + fieldsToShow.includes("blocker") || + fieldsToShow.includes("blocked") || + fieldsToShow.includes("dueDate"); + + const showThirdSection = + fieldsToShow.includes("all") || fieldsToShow.includes("cycle") || fieldsToShow.includes("module"); + + const minDate = issue.start_date ? new Date(issue.start_date) : null; + minDate?.setDate(minDate.getDate()); + + const maxDate = issue.target_date ? new Date(issue.target_date) : null; + maxDate?.setDate(maxDate.getDate()); + + const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); + + return ( + <> + {workspaceSlug && projectId && issue && ( + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issue} + onSubmit={async () => { + await issueOperations.remove(workspaceSlug, projectId, issueId); + router.push(`/${workspaceSlug}/projects/${projectId}/issues`); + }} + /> + )} + +
+
+
+ {currentIssueState ? ( + + ) : inboxIssueId ? ( + + ) : null} +

+ {projectDetails?.identifier}-{issue?.sequence_id} +

+
+ +
+ {currentUser && !is_archived && (fieldsToShow.includes("all") || fieldsToShow.includes("subscribe")) && ( + + )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( + + )} + + {is_editable && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && ( + + )} +
+
+ +
+
+ {showFirstSection && ( +
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( +
+
+ +

State

+
+ +
+ issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })} + projectId={projectId?.toString() ?? ""} + disabled={!is_editable} + buttonVariant="background-with-text" + /> +
+
+ )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( +
+
+ +

Assignees

+
+ +
+ + issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val }) + } + disabled={!is_editable} + projectId={projectId?.toString() ?? ""} + placeholder="Assignees" + multiple + buttonVariant={ + issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "background-with-text" + } + buttonClassName={issue?.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""} + /> +
+
+ )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && ( +
+
+ +

Priority

+
+ +
+ issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })} + disabled={!is_editable} + buttonVariant="background-with-text" + /> +
+
+ )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && + areEstimatesEnabledForCurrentProject && ( +
+
+ +

Estimate

+
+ +
+ + issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val }) + } + projectId={projectId} + disabled={!is_editable} + buttonVariant="background-with-text" + /> +
+
+ )} +
+ )} + + {showSecondSection && ( +
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( +
+
+ +

Parent

+
+
+ +
+
+ )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && ( + + )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && ( + + )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("duplicate")) && ( + + )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("relates_to")) && ( + + )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && ( +
+
+ +

Start date

+
+
+ + issueOperations.update(workspaceSlug, projectId, issueId, { start_date: val }) + } + className="border-none bg-custom-background-80" + maxDate={maxDate ?? undefined} + disabled={!is_editable} + /> +
+
+ )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && ( +
+
+ +

Due date

+
+
+ + issueOperations.update(workspaceSlug, projectId, issueId, { target_date: val }) + } + className="border-none bg-custom-background-80" + minDate={minDate ?? undefined} + disabled={!is_editable} + /> +
+
+ )} +
+ )} + + {showThirdSection && ( +
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && projectDetails?.cycle_view && ( +
+
+ +

Cycle

+
+
+ +
+
+ )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && ( +
+
+ +

Module

+
+
+ +
+
+ )} +
+ )} +
+ + {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( +
+
+ +

Label

+
+
+ +
+
+ )} + + {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( + + )} +
+
+ + ); +}); diff --git a/web/components/issues/issue-detail/subscription.tsx b/web/components/issues/issue-detail/subscription.tsx new file mode 100644 index 000000000..d20bc53cf --- /dev/null +++ b/web/components/issues/issue-detail/subscription.tsx @@ -0,0 +1,69 @@ +import { FC, useState } from "react"; +import { Bell } from "lucide-react"; +import { observer } from "mobx-react-lite"; +// UI +import { Button } from "@plane/ui"; +// hooks +import { useIssueDetail } from "hooks/store"; +import useToast from "hooks/use-toast"; + +export type TIssueSubscription = { + workspaceSlug: string; + projectId: string; + issueId: string; + currentUserId: string; +}; + +export const IssueSubscription: FC = observer((props) => { + const { workspaceSlug, projectId, issueId, currentUserId } = props; + // hooks + const { + issue: { getIssueById }, + subscription: { getSubscriptionByIssueId }, + createSubscription, + removeSubscription, + } = useIssueDetail(); + const { setToastAlert } = useToast(); + // state + const [loading, setLoading] = useState(false); + + const issue = getIssueById(issueId); + const subscription = getSubscriptionByIssueId(issueId); + + const handleSubscription = async () => { + setLoading(true); + try { + if (subscription?.subscribed) await removeSubscription(workspaceSlug, projectId, issueId); + else await createSubscription(workspaceSlug, projectId, issueId); + setToastAlert({ + type: "success", + title: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`, + message: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`, + }); + setLoading(false); + } catch (error) { + setLoading(false); + setToastAlert({ + type: "error", + title: "Error", + message: "Something went wrong. Please try again later.", + }); + } + }; + + if (issue?.created_by === currentUserId || issue?.assignee_ids?.includes(currentUserId)) return <>; + + return ( +
+ +
+ ); +}); diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 2d7f5005a..3b3ef887e 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; // components -import { CalendarChart, IssuePeekOverview } from "components/issues"; +import { CalendarChart } from "components/issues"; // hooks import useToast from "hooks/use-toast"; // types @@ -34,7 +34,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { // router const router = useRouter(); - const { workspaceSlug, projectId, peekIssueId, peekProjectId } = router.query; + const { workspaceSlug, projectId } = router.query; // hooks const { setToastAlert } = useToast(); @@ -113,16 +113,6 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { />
- {workspaceSlug && peekIssueId && peekProjectId && ( - - await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as TIssue, EIssueActions.UPDATE) - } - /> - )} ); }); diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 6bc3052a9..5b4885bf3 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -97,7 +97,7 @@ export const CalendarDayTile: React.FC = observer((props) => { formKey="target_date" groupId={formattedDatePayload} prePopulatedData={{ - target_date: renderFormattedPayloadDate(date.date), + target_date: renderFormattedPayloadDate(date.date) ?? undefined, }} quickAddCallback={quickAddCallback} viewId={viewId} diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index be30560fb..5711c89f6 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -1,15 +1,15 @@ import { useState, useRef } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Draggable } from "@hello-pangea/dnd"; import { MoreHorizontal } from "lucide-react"; // components -import { Tooltip } from "@plane/ui"; +import { Tooltip, ControlLink } from "@plane/ui"; // hooks import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// ui // types import { TIssue, TIssueMap } from "@plane/types"; -import { useProject, useProjectState } from "hooks/store"; +import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; type Props = { issues: TIssueMap | undefined; @@ -20,24 +20,24 @@ type Props = { export const CalendarIssueBlocks: React.FC = observer((props) => { const { issues, issueIdList, quickActions, showAllIssues = false } = props; - // router - const router = useRouter(); // hooks + const { + router: { workspaceSlug, projectId }, + } = useApplication(); const { getProjectById } = useProject(); const { getProjectStates } = useProjectState(); + const { setPeekIssue } = useIssueDetail(); // states const [isMenuActive, setIsMenuActive] = useState(false); const menuActionRef = useRef(null); - const handleIssuePeekOverview = (issue: TIssue) => { - const { query } = router; - - router.push({ - pathname: router.pathname, - query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id }, - }); - }; + const handleIssuePeekOverview = (issue: TIssue) => + workspaceSlug && + issue && + issue.project_id && + issue.id && + setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); @@ -59,6 +59,10 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { if (!issues?.[issueId]) return null; const issue = issues?.[issueId]; + + const stateColor = + getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || ""; + return ( {(provided, snapshot) => ( @@ -67,45 +71,51 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef} - onClick={() => handleIssuePeekOverview(issue)} > - {issue?.tempId !== undefined && ( -
- )} - -
handleIssuePeekOverview(issue)} + className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" > -
- state?.id == issue?.state_id - )?.color, - }} - /> -
- {getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id} + <> + {issue?.tempId !== undefined && ( +
+ )} + +
+
+ +
+ {getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id} +
+ +
{issue.name}
+
+
+
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + {quickActions(issue, customActionButton)} +
- -
{issue.name}
-
-
-
{ - e.preventDefault(); - e.stopPropagation(); - }} - > - {quickActions(issue, customActionButton)} -
-
+ +
)} diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 7a3c01417..0f81d79a6 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -110,11 +110,11 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { }, [errors, setToastAlert]); const onSubmitHandler = async (formData: TIssue) => { - if (isSubmitting || !groupId || !workspaceDetail || !projectDetail || !workspaceSlug || !projectId) return; + if (isSubmitting || !workspaceSlug || !projectId) return; reset({ ...defaultValues }); - const payload = createIssuePayload(workspaceDetail, projectDetail, { + const payload = createIssuePayload(projectId.toString(), { ...(prePopulatedData ?? {}), ...formData, }); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index 43e59dc76..573a9cf20 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -4,33 +4,28 @@ import { observer } from "mobx-react-lite"; import { useIssues } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; +import { BaseCalendarRoot } from "../base-calendar-root"; // types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; -import { BaseCalendarRoot } from "../base-calendar-root"; +// constants import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; -export const ProjectViewCalendarLayout: React.FC = observer(() => { +export interface IViewCalendarLayout { + issueActions: { + [EIssueActions.DELETE]: (issue: TIssue) => Promise; + [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; + [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + }; +} + +export const ProjectViewCalendarLayout: React.FC = observer((props) => { + const { issueActions } = props; + // store const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); + // router const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query; - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug.toString(), projectId.toString(), issue.id); - }, - }), - [issues, workspaceSlug, projectId] - ); + const { viewId } = router.query; return ( = observer((props) => { icon: , onClick: () => { setTrackElement("MODULE_EMPTY_STATE"); - toggleCreateIssueModal(true); + toggleCreateIssueModal(true, EIssuesStoreType.MODULE); }, }} secondaryButton={ diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 18eac8525..4ca2538e5 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -25,13 +25,14 @@ type Props = { handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void; labels?: IIssueLabel[] | undefined; states?: IState[] | undefined; + alwaysAllowEditing?: boolean; }; const membersFilters = ["assignees", "mentions", "created_by", "subscriber"]; const dateFilters = ["start_date", "target_date"]; export const AppliedFiltersList: React.FC = observer((props) => { - const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states } = props; + const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states, alwaysAllowEditing } = props; // store hooks const { membership: { currentProjectRole }, @@ -41,7 +42,7 @@ export const AppliedFiltersList: React.FC = observer((props) => { if (Object.keys(appliedFilters).length === 0) return null; - const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER); return (
diff --git a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx index 94ea9221e..ff5034c97 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx @@ -15,13 +15,13 @@ export const AppliedMembersFilters: React.FC = observer((props) => { const { handleRemove, values, editable } = props; const { - project: { getProjectMemberDetails }, + workspace: { getWorkspaceMemberDetails }, } = useMember(); return ( <> {values.map((memberId) => { - const memberDetails = getProjectMemberDetails(memberId)?.member; + const memberDetails = getWorkspaceMemberDetails(memberId)?.member; if (!memberDetails) return null; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index 87bb719c4..4d8ad5196 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -1,32 +1,48 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import isEqual from "lodash/isEqual"; // hooks -import { useIssues, useLabel } from "hooks/store"; +import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store"; +//ui +import { Button } from "@plane/ui"; // components import { AppliedFiltersList } from "components/issues"; // types -import { IIssueFilterOptions } from "@plane/types"; +import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; -export const GlobalViewsAppliedFiltersRoot = observer(() => { +type Props = { + globalViewId: string; +}; + +export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => { + const { globalViewId } = props; // router const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; + const { workspaceSlug } = router.query; // store hooks const { - issuesFilter: { issueFilters, updateFilters }, + issuesFilter: { filters, updateFilters }, } = useIssues(EIssuesStoreType.GLOBAL); const { workspace: { workspaceLabels }, } = useLabel(); + const { globalViewMap, updateGlobalView } = useGlobalView(); + const { + membership: { currentWorkspaceRole }, + } = useUser(); + // derived values - const userFilters = issueFilters?.filters; + const userFilters = filters?.[globalViewId]?.filters; + const viewDetails = globalViewMap[globalViewId]; // filters whose value not null or empty array - const appliedFilters: IIssueFilterOptions = {}; + let appliedFilters: IIssueFilterOptions | undefined = undefined; Object.entries(userFilters ?? {}).forEach(([key, value]) => { if (!value) return; if (Array.isArray(value) && value.length === 0) return; + if (!appliedFilters) appliedFilters = {}; appliedFilters[key as keyof IIssueFilterOptions] = value; }); @@ -70,29 +86,24 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => { ); }; - // const handleUpdateView = () => { - // if (!workspaceSlug || !globalViewId || !viewDetails) return; + const handleUpdateView = () => { + if (!workspaceSlug || !globalViewId) return; - // globalViewsStore.updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), { - // query_data: { - // ...viewDetails.query_data, - // filters: { - // ...(storedFilters ?? {}), - // }, - // }, - // }); - // }; + updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), { + filters: { + ...(appliedFilters ?? {}), + }, + }); + }; - // update stored filters when view details are fetched - // useEffect(() => { - // if (!globalViewId || !viewDetails) return; + const areFiltersEqual = isEqual(appliedFilters, viewDetails?.filters); - // if (!globalViewFiltersStore.storedFilters[globalViewId.toString()]) - // globalViewFiltersStore.updateStoredFilters(globalViewId.toString(), viewDetails?.query_data?.filters ?? {}); - // }, [globalViewId, globalViewFiltersStore, viewDetails]); + const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + + const isDefaultView = DEFAULT_GLOBAL_VIEWS_LIST.map((view) => view.key).includes(globalViewId as TStaticViewTypes); // return if no filters are applied - if (Object.keys(appliedFilters).length === 0) return null; + if (!appliedFilters && areFiltersEqual) return null; return (
@@ -101,13 +112,17 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => { appliedFilters={appliedFilters ?? {}} handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} + alwaysAllowEditing /> - {/* {storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data.filters ?? {}) && ( - - )} */} + {!isDefaultView && !areFiltersEqual && isAuthorizedUser && ( + <> +
+ + + )}
); }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index ffbae9ac8..c10de461a 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -1,13 +1,12 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import isEqual from "lodash/isEqual"; // hooks import { useIssues, useLabel, useProjectState, useProjectView } from "hooks/store"; // components import { AppliedFiltersList } from "components/issues"; // ui import { Button } from "@plane/ui"; -// helpers -import { areFiltersDifferent } from "helpers/filter.helper"; // types import { IIssueFilterOptions } from "@plane/types"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; @@ -28,33 +27,46 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { project: { projectLabels }, } = useLabel(); const { projectStates } = useProjectState(); - const { getViewById, updateView } = useProjectView(); + const { viewMap, updateView } = useProjectView(); // derived values - const viewDetails = viewId ? getViewById(viewId.toString()) : null; + const viewDetails = viewId ? viewMap[viewId.toString()] : null; const userFilters = issueFilters?.filters; // filters whose value not null or empty array - const appliedFilters: IIssueFilterOptions = {}; + let appliedFilters: IIssueFilterOptions | undefined = undefined; Object.entries(userFilters ?? {}).forEach(([key, value]) => { if (!value) return; if (Array.isArray(value) && value.length === 0) return; + if (!appliedFilters) appliedFilters = {}; appliedFilters[key as keyof IIssueFilterOptions] = value; }); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { if (!workspaceSlug || !projectId) return; if (!value) { - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { - [key]: null, - }); + updateFilters( + workspaceSlug, + projectId, + EIssueFilterType.FILTERS, + { + [key]: null, + }, + viewId + ); return; } let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { - [key]: newValues, - }); + updateFilters( + workspaceSlug, + projectId, + EIssueFilterType.FILTERS, + { + [key]: newValues, + }, + viewId + ); }; const handleClearAllFilters = () => { @@ -66,15 +78,15 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, viewId); }; + const areFiltersEqual = isEqual(appliedFilters, viewDetails?.filters); // return if no filters are applied - if (Object.keys(appliedFilters).length === 0) return null; + if (!appliedFilters && areFiltersEqual) return null; const handleUpdateView = () => { if (!workspaceSlug || !projectId || !viewId || !viewDetails) return; updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), { - query_data: { - ...viewDetails.query_data, + filters: { ...(appliedFilters ?? {}), }, }); @@ -83,22 +95,24 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { return (
- {appliedFilters && - viewDetails?.query_data && - areFiltersDifferent(appliedFilters, viewDetails?.query_data ?? {}) && ( + {!areFiltersEqual && ( + <> +
- )} + + )}
); }); diff --git a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx index 657251732..ea9097146 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx @@ -5,8 +5,8 @@ import { observer } from "mobx-react-lite"; import { FilterHeader, FilterOption } from "components/issues"; // icons import { StateGroupIcon } from "@plane/ui"; +import { STATE_GROUPS } from "constants/state"; // constants -import { ISSUE_STATE_GROUPS } from "constants/issue"; type Props = { appliedFilters: string[] | null; @@ -22,7 +22,7 @@ export const FilterStateGroup: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = ISSUE_STATE_GROUPS.filter((s) => s.key.includes(searchQuery.toLowerCase())); + const filteredOptions = Object.values(STATE_GROUPS).filter((s) => s.key.includes(searchQuery.toLowerCase())); const handleViewToggle = () => { if (!filteredOptions) return; @@ -48,7 +48,7 @@ export const FilterStateGroup: React.FC = observer((props) => { isChecked={appliedFilters?.includes(stateGroup.key) ? true : false} onClick={() => handleUpdate(stateGroup.key)} icon={} - title={stateGroup.title} + title={stateGroup.label} /> ))} {filteredOptions.length > 5 && ( diff --git a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx index 9c0ef8511..425f53b46 100644 --- a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx +++ b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx @@ -12,10 +12,11 @@ type Props = { title?: string; placement?: Placement; disabled?: boolean; + tabIndex?: number; }; export const FiltersDropdown: React.FC = (props) => { - const { children, title = "Dropdown", placement, disabled = false } = props; + const { children, title = "Dropdown", placement, disabled = false, tabIndex } = props; const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -40,6 +41,7 @@ export const FiltersDropdown: React.FC = (props) => { appendIcon={ } + tabIndex={tabIndex} >
{title} diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 13b324282..73802886e 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -1,10 +1,10 @@ -import React, { useCallback } from "react"; +import React from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks import { useIssues, useUser } from "hooks/store"; // components -import { IssueGanttBlock, IssuePeekOverview } from "components/issues"; +import { IssueGanttBlock } from "components/issues"; import { GanttChartRoot, IBlockUpdateData, @@ -32,10 +32,10 @@ interface IBaseGanttRoot { } export const BaseGanttRoot: React.FC = observer((props: IBaseGanttRoot) => { - const { issueFiltersStore, issueStore, viewId, issueActions } = props; + const { issueFiltersStore, issueStore, viewId } = props; // router const router = useRouter(); - const { workspaceSlug, peekIssueId, peekProjectId } = router.query; + const { workspaceSlug } = router.query; // store hooks const { membership: { currentProjectRole }, @@ -57,14 +57,6 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan await issueStore.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, payload, viewId); }; - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - await issueActions[action]!(issue); - } - }, - [issueActions] - ); const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return ( @@ -92,16 +84,6 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed} />
- {workspaceSlug && peekIssueId && peekProjectId && ( - { - await handleIssues(issueToUpdate as TIssue, action); - }} - /> - )} ); }); diff --git a/web/components/issues/issue-layouts/gantt/blocks.tsx b/web/components/issues/issue-layouts/gantt/blocks.tsx index fd79cdde1..fefee880d 100644 --- a/web/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/components/issues/issue-layouts/gantt/blocks.tsx @@ -1,31 +1,34 @@ import { useRouter } from "next/router"; // ui -import { Tooltip, StateGroupIcon } from "@plane/ui"; +import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; // types import { TIssue } from "@plane/types"; -import { useProject, useProjectState } from "hooks/store"; +import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; export const IssueGanttBlock = ({ data }: { data: TIssue }) => { - const router = useRouter(); // hooks + const { + router: { workspaceSlug }, + } = useApplication(); const { getProjectStates } = useProjectState(); + const { setPeekIssue } = useIssueDetail(); - const handleIssuePeekOverview = () => { - const { query } = router; + const handleIssuePeekOverview = () => + workspaceSlug && + data && + data.project_id && + data.id && + setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id }); - router.push({ - pathname: router.pathname, - query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project_id }, - }); - }; + const stateColor = getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id)?.color || ""; return (
state?.id == data?.state_id)?.color, + backgroundColor: stateColor, }} onClick={handleIssuePeekOverview} > @@ -49,34 +52,42 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => { // rendering issues on gantt sidebar export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => { - const router = useRouter(); // hooks const { getProjectStates } = useProjectState(); const { getProjectById } = useProject(); + const { + router: { workspaceSlug }, + } = useApplication(); + const { setPeekIssue } = useIssueDetail(); - const handleIssuePeekOverview = () => { - const { query } = router; - - router.push({ - pathname: router.pathname, - query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project_id }, - }); - }; + const handleIssuePeekOverview = () => + workspaceSlug && + data && + data.project_id && + data.id && + setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id }); const currentStateDetails = getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id) || undefined; return ( -
- {currentStateDetails != undefined && ( - - )} -
- {getProjectById(data?.project_id)?.identifier} {data?.sequence_id} + +
+ {currentStateDetails != undefined && ( + + )} +
+ {getProjectById(data?.project_id)?.identifier} {data?.sequence_id} +
+ + {data?.name} +
- - {data?.name} - -
+ ); }; diff --git a/web/components/issues/issue-layouts/gantt/project-view-root.tsx b/web/components/issues/issue-layouts/gantt/project-view-root.tsx index a5cc57c4e..1ed02c2c9 100644 --- a/web/components/issues/issue-layouts/gantt/project-view-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-view-root.tsx @@ -4,28 +4,34 @@ import { observer } from "mobx-react-lite"; import { useIssues } from "hooks/store"; // components import { BaseGanttRoot } from "./base-gantt-root"; +// constants import { EIssuesStoreType } from "constants/issue"; +// types import { EIssueActions } from "../types"; import { TIssue } from "@plane/types"; -export const ProjectViewGanttLayout: React.FC = observer(() => { +export interface IViewGanttLayout { + issueActions: { + [EIssueActions.DELETE]: (issue: TIssue) => Promise; + [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; + [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + }; +} + +export const ProjectViewGanttLayout: React.FC = observer((props) => { + const { issueActions } = props; + // store const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { viewId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }; - - return ; + return ( + + ); }); diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index 621a12d76..78f332ddd 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -1,10 +1,10 @@ -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState, useRef, FC } from "react"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks -import { useProject, useWorkspace } from "hooks/store"; +import { useProject } from "hooks/store"; import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -12,9 +12,38 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { createIssuePayload } from "helpers/issue.helper"; // types -import { TIssue } from "@plane/types"; +import { IProject, TIssue } from "@plane/types"; -type Props = { +interface IInputProps { + formKey: string; + register: any; + setFocus: any; + projectDetail: IProject | null; +} +const Inputs: FC = (props) => { + const { formKey, register, setFocus, projectDetail } = props; + + useEffect(() => { + setFocus(formKey); + }, [formKey, setFocus]); + + return ( +
+
{projectDetail?.identifier ?? "..."}
+ +
+ ); +}; + +type IGanttQuickAddIssueForm = { prePopulatedData?: Partial; onSuccess?: (data: TIssue) => Promise | void; quickAddCallback?: ( @@ -30,34 +59,25 @@ const defaultValues: Partial = { name: "", }; -const Inputs = (props: any) => { - const { register, setFocus } = props; - - useEffect(() => { - setFocus("name"); - }, [setFocus]); - - return ( - - ); -}; - -export const GanttInlineCreateIssueForm: React.FC = observer((props) => { +export const GanttQuickAddIssueForm: React.FC = observer((props) => { const { prePopulatedData, quickAddCallback, viewId } = props; // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // store hooks - const { getWorkspaceBySlug } = useWorkspace(); - const { currentProjectDetails } = useProject(); + // hooks + const { getProjectById } = useProject(); + const { setToastAlert } = useToast(); + + const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined; + + const ref = useRef(null); + + const [isOpen, setIsOpen] = useState(false); + const handleClose = () => setIsOpen(false); + + useKeypress("Escape", handleClose); + useOutsideClickDetector(ref, handleClose); + // form info const { reset, @@ -67,106 +87,67 @@ export const GanttInlineCreateIssueForm: React.FC = observer((props) => { formState: { errors, isSubmitting }, } = useForm({ defaultValues }); - // ref - const ref = useRef(null); - - // states - const [isOpen, setIsOpen] = useState(false); - - const handleClose = () => setIsOpen(false); - - // hooks - useKeypress("Escape", handleClose); - useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); - - // derived values - const workspaceDetail = getWorkspaceBySlug(workspaceSlug?.toString()!); - useEffect(() => { if (!isOpen) reset({ ...defaultValues }); }, [isOpen, reset]); - useEffect(() => { - if (!errors) return; - - Object.keys(errors).forEach((key) => { - const error = errors[key as keyof TIssue]; - - setToastAlert({ - type: "error", - title: "Error!", - message: error?.message?.toString() || "Some error occurred. Please try again.", - }); - }); - }, [errors, setToastAlert]); - const onSubmitHandler = async (formData: TIssue) => { if (isSubmitting || !workspaceSlug || !projectId) return; - // resetting the form so that user can add another issue quickly - reset({ ...defaultValues, ...(prePopulatedData ?? {}) }); + reset({ ...defaultValues }); - const payload = createIssuePayload(workspaceDetail!, currentProjectDetails!, { + const targetDate = new Date(); + targetDate.setDate(targetDate.getDate() + 1); + + const payload = createIssuePayload(projectId.toString(), { ...(prePopulatedData ?? {}), ...formData, start_date: renderFormattedPayloadDate(new Date()), - target_date: renderFormattedPayloadDate(new Date(new Date().getTime() + 24 * 60 * 60 * 1000)), + target_date: renderFormattedPayloadDate(targetDate), }); try { - if (quickAddCallback) { - await quickAddCallback(workspaceSlug.toString(), projectId.toString(), payload, viewId); - } + quickAddCallback && + (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId)); setToastAlert({ type: "success", title: "Success!", message: "Issue created successfully.", }); } catch (err: any) { - Object.keys(err || {}).forEach((key) => { - const error = err?.[key]; - const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null; - - setToastAlert({ - type: "error", - title: "Error!", - message: errorTitle || "Some error occurred. Please try again.", - }); + setToastAlert({ + type: "error", + title: "Error!", + message: err?.message || "Some error occurred. Please try again.", }); } }; - return ( <> - {isOpen && ( -
-
-

{currentProjectDetails?.identifier ?? "..."}

- - - )} - - {isOpen && ( -

- Press {"'"}Enter{"'"} to add another issue -

- )} - - {!isOpen && ( - - )} +
+ {isOpen ? ( +
+
+ + +
{`Press 'Enter' to add another issue`}
+
+ ) : ( +
setIsOpen(true)} + > + + New Issue +
+ )} +
); }); diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index dcf6e7cc3..4c2e21649 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -15,17 +15,16 @@ import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; //components import { KanBan } from "./default"; import { KanBanSwimLanes } from "./swimlanes"; -import { DeleteIssueModal, IssuePeekOverview } from "components/issues"; +import { DeleteIssueModal } from "components/issues"; import { EUserProjectRoles } from "constants/project"; import { useIssues } from "hooks/store/use-issues"; import { handleDragDrop } from "./utils"; -import { IssueKanBanViewStore } from "store/issue/issue_kanban_view.store"; import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { TCreateModalStoreTypes } from "constants/issue"; +import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue"; export interface IBaseKanBanLayout { issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues; @@ -44,7 +43,7 @@ export interface IBaseKanBanLayout { }; showLoader?: boolean; viewId?: string; - currentStore?: TCreateModalStoreTypes; + storeType?: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; } @@ -63,13 +62,13 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas issueActions, showLoader, viewId, - currentStore, + storeType, addIssuesToView, canEditPropertiesBasedOnProject, } = props; // router const router = useRouter(); - const { workspaceSlug, projectId, peekIssueId, peekProjectId } = router.query; + const { workspaceSlug, projectId } = router.query; // store hooks const { membership: { currentProjectRole }, @@ -78,9 +77,6 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas // toast alert const { setToastAlert } = useToast(); - // FIXME get from filters - const kanbanViewStore: IssueKanBanViewStore = {} as IssueKanBanViewStore; - const issueIds = issues?.groupedIssueIds || []; const displayFilters = issuesFilter?.issueFilters?.displayFilters; @@ -206,15 +202,25 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas issueIds, viewId ).finally(() => { + handleIssues(issueMap[dragState.draggedIssueId!], EIssueActions.DELETE); setDeleteIssueModal(false); setDragState({}); }); }; - const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { - kanbanViewStore.handleKanBanToggle(toggle, value); + const handleKanbanFilters = (toggle: "group_by" | "sub_group_by", value: string) => { + if (workspaceSlug && projectId) { + let _kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; + if (_kanbanFilters.includes(value)) _kanbanFilters = _kanbanFilters.filter((_value) => _value != value); + else _kanbanFilters.push(value); + issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.KANBAN_FILTERS, { + [toggle]: _kanbanFilters, + }); + } }; + const kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] }; + return ( <> = observer((props: IBas
)} -
+
+ {/* drag and delete component */}
= observer((props: IBas group_by={group_by} handleIssues={handleIssues} quickActions={renderQuickActions} - kanBanToggle={kanbanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} + handleKanbanFilters={handleKanbanFilters} + kanbanFilters={kanbanFilters} enableQuickIssueCreate={enableQuickAdd} showEmptyGroup={userDisplayFilters?.show_empty_groups || true} quickAddCallback={issues?.quickAddIssue} viewId={viewId} disableIssueCreation={!enableIssueCreation || !isEditingAllowed} canEditProperties={canEditProperties} - currentStore={currentStore} + storeType={storeType} addIssuesToView={addIssuesToView} />
- - {workspaceSlug && peekIssueId && peekProjectId && ( - await handleIssues(issueToUpdate as TIssue, EIssueActions.UPDATE)} - /> - )} ); }); diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 682f3c416..40bd19cf1 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -5,12 +5,11 @@ import { observer } from "mobx-react-lite"; import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { IssueProperties } from "../properties/all-properties"; // ui -import { Tooltip } from "@plane/ui"; +import { Tooltip, ControlLink } from "@plane/ui"; // types import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; import { EIssueActions } from "../types"; -import { useRouter } from "next/router"; -import { useProject } from "hooks/store"; +import { useApplication, useIssueDetail, useProject } from "hooks/store"; interface IssueBlockProps { issueId: string; @@ -34,24 +33,23 @@ interface IssueDetailsBlockProps { const KanbanIssueDetailsBlock: React.FC = observer((props: IssueDetailsBlockProps) => { const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props; - - const router = useRouter(); - // hooks const { getProjectById } = useProject(); + const { + router: { workspaceSlug, projectId }, + } = useApplication(); + const { setPeekIssue } = useIssueDetail(); const updateIssue = (issueToUpdate: TIssue) => { if (issueToUpdate) handleIssues(issueToUpdate, EIssueActions.UPDATE); }; - const handleIssuePeekOverview = () => { - const { query } = router; - - router.push({ - pathname: router.pathname, - query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id }, - }); - }; + const handleIssuePeekOverview = (issue: TIssue) => + workspaceSlug && + issue && + issue.project_id && + issue.id && + setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); return ( <> @@ -63,11 +61,18 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop
{quickActions(issue)}
- -
- {issue.name} -
-
+ + handleIssuePeekOverview(issue)} + className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + > + + {issue.name} + + + void; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; - kanBanToggle: any; - handleKanBanToggle: any; + kanbanFilters: TIssueKanbanFilters; + handleKanbanFilters: any; enableQuickIssueCreate?: boolean; quickAddCallback?: ( workspaceSlug: string, @@ -41,7 +42,7 @@ export interface IGroupByKanBan { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - currentStore?: TCreateModalStoreTypes; + storeType?: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; } @@ -57,13 +58,13 @@ const GroupByKanBan: React.FC = observer((props) => { isDragDisabled, handleIssues, quickActions, - kanBanToggle, - handleKanBanToggle, + kanbanFilters, + handleKanbanFilters, enableQuickIssueCreate, quickAddCallback, viewId, disableIssueCreation, - currentStore, + storeType, addIssuesToView, canEditProperties, } = props; @@ -77,53 +78,63 @@ const GroupByKanBan: React.FC = observer((props) => { if (!list) return null; - const verticalAlignPosition = (_list: IGroupByColumn) => kanBanToggle?.groupByHeaderMinMax.includes(_list.id); + const visibilityGroupBy = (_list: IGroupByColumn) => + sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false; + + const isGroupByCreatedBy = group_by === "created_by"; return ( -
+
{list && list.length > 0 && list.map((_list: IGroupByColumn) => { - const verticalPosition = verticalAlignPosition(_list); + const groupByVisibilityToggle = visibilityGroupBy(_list); return ( -
+
{sub_group_by === null && ( -
+
)} - + + {!groupByVisibilityToggle && ( + + )}
); })} @@ -140,8 +151,8 @@ export interface IKanBan { sub_group_id?: string; handleIssues: (issue: TIssue, action: EIssueActions) => void; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; - kanBanToggle: any; - handleKanBanToggle: any; + kanbanFilters: TIssueKanbanFilters; + handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; showEmptyGroup: boolean; enableQuickIssueCreate?: boolean; quickAddCallback?: ( @@ -152,7 +163,7 @@ export interface IKanBan { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - currentStore?: TCreateModalStoreTypes; + storeType?: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; } @@ -167,13 +178,13 @@ export const KanBan: React.FC = observer((props) => { sub_group_id = "null", handleIssues, quickActions, - kanBanToggle, - handleKanBanToggle, + kanbanFilters, + handleKanbanFilters, enableQuickIssueCreate, quickAddCallback, viewId, disableIssueCreation, - currentStore, + storeType, addIssuesToView, canEditProperties, } = props; @@ -181,27 +192,25 @@ export const KanBan: React.FC = observer((props) => { const issueKanBanView = useKanbanView(); return ( -
- -
+ ); }); diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index 4d4776d38..3bb49106b 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -11,7 +11,7 @@ import useToast from "hooks/use-toast"; // mobx import { observer } from "mobx-react-lite"; // types -import { TIssue, ISearchIssueResponse } from "@plane/types"; +import { TIssue, ISearchIssueResponse, TIssueKanbanFilters } from "@plane/types"; import { TCreateModalStoreTypes } from "constants/issue"; interface IHeaderGroupByCard { @@ -21,11 +21,11 @@ interface IHeaderGroupByCard { icon?: React.ReactNode; title: string; count: number; - kanBanToggle: any; - handleKanBanToggle: any; + kanbanFilters: TIssueKanbanFilters; + handleKanbanFilters: any; issuePayload: Partial; disableIssueCreation?: boolean; - currentStore?: TCreateModalStoreTypes; + storeType?: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; } @@ -36,14 +36,14 @@ export const HeaderGroupByCard: FC = observer((props) => { icon, title, count, - kanBanToggle, - handleKanBanToggle, + kanbanFilters, + handleKanbanFilters, issuePayload, disableIssueCreation, - currentStore, + storeType, addIssuesToView, } = props; - const verticalAlignPosition = kanBanToggle?.groupByHeaderMinMax.includes(column_id); + const verticalAlignPosition = sub_group_by ? false : kanbanFilters?.group_by.includes(column_id); const [isOpen, setIsOpen] = React.useState(false); const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false); @@ -84,7 +84,12 @@ export const HeaderGroupByCard: FC = observer((props) => { fieldsToShow={["all"]} /> ) : ( - setIsOpen(false)} data={issuePayload} /> + setIsOpen(false)} + data={issuePayload} + storeType={storeType} + /> )} {renderExistingIssueModal && ( = observer((props) => { {sub_group_by === null && (
handleKanBanToggle("groupByHeaderMinMax", column_id)} + onClick={() => handleKanbanFilters("group_by", column_id)} > {verticalAlignPosition ? ( diff --git a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx index de5e7abc4..ea9464780 100644 --- a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx @@ -1,26 +1,26 @@ import React from "react"; -// lucide icons import { Circle, ChevronDown, ChevronUp } from "lucide-react"; // mobx import { observer } from "mobx-react-lite"; +import { TIssueKanbanFilters } from "@plane/types"; interface IHeaderSubGroupByCard { icon?: React.ReactNode; title: string; count: number; column_id: string; - kanBanToggle: any; - handleKanBanToggle: any; + kanbanFilters: TIssueKanbanFilters; + handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; } export const HeaderSubGroupByCard = observer( - ({ icon, title, count, column_id, kanBanToggle, handleKanBanToggle }: IHeaderSubGroupByCard) => ( + ({ icon, title, count, column_id, kanbanFilters, handleKanbanFilters }: IHeaderSubGroupByCard) => (
handleKanBanToggle("subgroupByIssuesVisibility", column_id)} + onClick={() => handleKanbanFilters("sub_group_by", column_id)} > - {kanBanToggle?.subgroupByIssuesVisibility.includes(column_id) ? ( + {kanbanFilters?.sub_group_by.includes(column_id) ? ( ) : ( diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index ce0a4d105..0858190d3 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -1,4 +1,8 @@ import { Droppable } from "@hello-pangea/dnd"; +// hooks +import { useProjectState } from "hooks/store"; +//components +import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; //types import { TGroupedIssues, @@ -9,8 +13,6 @@ import { TUnGroupedIssues, } from "@plane/types"; import { EIssueActions } from "../types"; -//components -import { KanBanQuickAddIssueForm, KanbanIssueBlocksList } from "."; interface IKanbanGroup { groupId: string; @@ -33,7 +35,7 @@ interface IKanbanGroup { viewId?: string; disableIssueCreation?: boolean; canEditProperties: (projectId: string | undefined) => boolean; - verticalPosition: any; + groupByVisibilityToggle: boolean; } export const KanbanGroup = (props: IKanbanGroup) => { @@ -44,7 +46,6 @@ export const KanbanGroup = (props: IKanbanGroup) => { sub_group_by, issuesMap, displayProperties, - verticalPosition, issueIds, isDragDisabled, handleIssues, @@ -55,52 +56,95 @@ export const KanbanGroup = (props: IKanbanGroup) => { quickAddCallback, viewId, } = props; + // hooks + const projectState = useProjectState(); + + const prePopulateQuickAddData = ( + groupByKey: string | null, + subGroupByKey: string | null, + groupValue: string, + subGroupValue: string + ) => { + const defaultState = projectState.projectStates?.find((state) => state.default); + let preloadedData: object = { state_id: defaultState?.id }; + + if (groupByKey) { + if (groupByKey === "state") { + preloadedData = { ...preloadedData, state_id: groupValue }; + } else if (groupByKey === "priority") { + preloadedData = { ...preloadedData, priority: groupValue }; + } else if (groupByKey === "labels" && groupValue != "None") { + preloadedData = { ...preloadedData, label_ids: [groupValue] }; + } else if (groupByKey === "assignees" && groupValue != "None") { + preloadedData = { ...preloadedData, assignee_ids: [groupValue] }; + } else if (groupByKey === "created_by") { + preloadedData = { ...preloadedData }; + } else { + preloadedData = { ...preloadedData, [groupByKey]: groupValue }; + } + } + + if (subGroupByKey) { + if (subGroupByKey === "state") { + preloadedData = { ...preloadedData, state_id: subGroupValue }; + } else if (subGroupByKey === "priority") { + preloadedData = { ...preloadedData, priority: subGroupValue }; + } else if (subGroupByKey === "labels" && subGroupValue != "None") { + preloadedData = { ...preloadedData, label_ids: [subGroupValue] }; + } else if (subGroupByKey === "assignees" && subGroupValue != "None") { + preloadedData = { ...preloadedData, assignee_ids: [subGroupValue] }; + } else if (subGroupByKey === "created_by") { + preloadedData = { ...preloadedData }; + } else { + preloadedData = { ...preloadedData, [subGroupByKey]: subGroupValue }; + } + } + + return preloadedData; + }; return ( -
+
{(provided: any, snapshot: any) => (
- {!verticalPosition ? ( - - ) : null} + {provided.placeholder} + + {enableQuickIssueCreate && !disableIssueCreation && ( +
+ +
+ )}
)}
- -
- {enableQuickIssueCreate && !disableIssueCreation && ( - - )} -
); }; diff --git a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx index 07c5fbda6..21aeb3d9d 100644 --- a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx @@ -4,7 +4,7 @@ import { useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks -import { useProject, useWorkspace } from "hooks/store"; +import { useProject } from "hooks/store"; import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -59,10 +59,8 @@ export const KanBanQuickAddIssueForm: React.FC = obser const router = useRouter(); const { workspaceSlug, projectId } = router.query; // store hooks - const { getWorkspaceBySlug } = useWorkspace(); const { getProjectById } = useProject(); - const workspaceDetail = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : null; const projectDetail = projectId ? getProjectById(projectId.toString()) : null; const ref = useRef(null); @@ -87,11 +85,11 @@ export const KanBanQuickAddIssueForm: React.FC = obser }, [isOpen, reset]); const onSubmitHandler = async (formData: TIssue) => { - if (isSubmitting || !groupId || !workspaceDetail || !projectDetail || !workspaceSlug || !projectId) return; + if (isSubmitting || !workspaceSlug || !projectId) return; reset({ ...defaultValues }); - const payload = createIssuePayload(workspaceDetail, projectDetail, { + const payload = createIssuePayload(projectId.toString(), { ...(prePopulatedData ?? {}), ...formData, }); @@ -122,13 +120,13 @@ export const KanBanQuickAddIssueForm: React.FC = obser }; return ( -
+ <> {isOpen ? ( -
+
@@ -143,33 +141,6 @@ export const KanBanQuickAddIssueForm: React.FC = obser New Issue
)} - - {/* {isOpen && ( -
- - - )} - - {isOpen && ( -

- Press {"'"}Enter{"'"} to add another issue -

- )} - - {!isOpen && ( - - )} */} -
+ ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index ae0d4a1bb..0903355ce 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -50,7 +50,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { showLoader={true} QuickActions={CycleIssueQuickActions} viewId={cycleId?.toString() ?? ""} - currentStore={EIssuesStoreType.CYCLE} + storeType={EIssuesStoreType.CYCLE} addIssuesToView={(issueIds: string[]) => { if (!workspaceSlug || !projectId || !cycleId) throw new Error(); return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 4577d56b7..89f4683af 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -50,7 +50,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { showLoader={true} QuickActions={ModuleIssueQuickActions} viewId={moduleId?.toString()} - currentStore={EIssuesStoreType.MODULE} + storeType={EIssuesStoreType.MODULE} addIssuesToView={(issueIds: string[]) => { if (!workspaceSlug || !projectId || !moduleId) throw new Error(); return issues.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds); diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index c0bf0a728..4b3f3fe83 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -52,7 +52,7 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => { issues={issues} showLoader={true} QuickActions={ProjectIssueQuickActions} - currentStore={EIssuesStoreType.PROFILE} + storeType={EIssuesStoreType.PROFILE} canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject} /> ); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx index b67ceb460..89e2ee187 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -43,7 +43,7 @@ export const KanBanLayout: React.FC = observer(() => { issuesFilter={issuesFilter} showLoader={true} QuickActions={ProjectIssueQuickActions} - currentStore={EIssuesStoreType.PROJECT} + storeType={EIssuesStoreType.PROJECT} /> ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index 86f6a281b..1cdf71d45 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -1,38 +1,32 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks import { useIssues } from "hooks/store"; // constant +import { EIssuesStoreType } from "constants/issue"; +// types import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; // components import { BaseKanBanRoot } from "../base-kanban-root"; -import { EIssuesStoreType } from "constants/issue"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -export interface IViewKanBanLayout {} +export interface IViewKanBanLayout { + issueActions: { + [EIssueActions.DELETE]: (issue: TIssue) => Promise; + [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; + [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + }; +} -export const ProjectViewKanBanLayout: React.FC = observer(() => { +export const ProjectViewKanBanLayout: React.FC = observer((props) => { + const { issueActions } = props; + // router const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string }; + const { viewId } = router.query; const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); return ( { issues={issues} showLoader={true} QuickActions={ProjectIssueQuickActions} - currentStore={EIssuesStoreType.PROJECT_VIEW} + storeType={EIssuesStoreType.PROJECT_VIEW} + viewId={viewId?.toString()} /> ); }); diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index d65bbee57..33126d17b 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -13,6 +13,7 @@ import { IIssueMap, TSubGroupedIssues, TUnGroupedIssues, + TIssueKanbanFilters, } from "@plane/types"; // constants import { EIssueActions } from "../types"; @@ -25,16 +26,16 @@ interface ISubGroupSwimlaneHeader { sub_group_by: string | null; group_by: string | null; list: IGroupByColumn[]; - kanBanToggle: any; - handleKanBanToggle: any; + kanbanFilters: TIssueKanbanFilters; + handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; } const SubGroupSwimlaneHeader: React.FC = ({ issueIds, sub_group_by, group_by, list, - kanBanToggle, - handleKanBanToggle, + kanbanFilters, + handleKanbanFilters, }) => (
{list && @@ -45,11 +46,11 @@ const SubGroupSwimlaneHeader: React.FC = ({ sub_group_by={sub_group_by} group_by={group_by} column_id={_list.id} - icon={_list.Icon} + icon={_list.icon} title={_list.name} count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0} - kanBanToggle={kanBanToggle} - handleKanBanToggle={handleKanBanToggle} + kanbanFilters={kanbanFilters} + handleKanbanFilters={handleKanbanFilters} issuePayload={_list.payload} />
@@ -64,11 +65,11 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { displayProperties: IIssueDisplayProperties | undefined; handleIssues: (issue: TIssue, action: EIssueActions) => void; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; - kanBanToggle: any; - handleKanBanToggle: any; + kanbanFilters: TIssueKanbanFilters; + handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; isDragStarted?: boolean; disableIssueCreation?: boolean; - currentStore?: TCreateModalStoreTypes; + storeType?: TCreateModalStoreTypes; enableQuickIssueCreate: boolean; canEditProperties: (projectId: string | undefined) => boolean; addIssuesToView?: (issueIds: string[]) => Promise; @@ -90,8 +91,8 @@ const SubGroupSwimlane: React.FC = observer((props) => { handleIssues, quickActions, displayProperties, - kanBanToggle, - handleKanBanToggle, + kanbanFilters, + handleKanbanFilters, showEmptyGroup, enableQuickIssueCreate, canEditProperties, @@ -123,13 +124,14 @@ const SubGroupSwimlane: React.FC = observer((props) => { icon={_list.Icon} title={_list.name || ""} count={calculateIssueCount(_list.id)} - kanBanToggle={kanBanToggle} - handleKanBanToggle={handleKanBanToggle} + kanbanFilters={kanbanFilters} + handleKanbanFilters={handleKanbanFilters} />
- {!kanBanToggle?.subgroupByIssuesVisibility.includes(_list.id) && ( + + {!kanbanFilters?.sub_group_by.includes(_list.id) && (
= observer((props) => { sub_group_id={_list.id} handleIssues={handleIssues} quickActions={quickActions} - kanBanToggle={kanBanToggle} - handleKanBanToggle={handleKanBanToggle} + kanbanFilters={kanbanFilters} + handleKanbanFilters={handleKanbanFilters} showEmptyGroup={showEmptyGroup} enableQuickIssueCreate={enableQuickIssueCreate} canEditProperties={canEditProperties} @@ -165,12 +167,12 @@ export interface IKanBanSwimLanes { group_by: string | null; handleIssues: (issue: TIssue, action: EIssueActions) => void; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; - kanBanToggle: any; - handleKanBanToggle: any; + kanbanFilters: TIssueKanbanFilters; + handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; showEmptyGroup: boolean; isDragStarted?: boolean; disableIssueCreation?: boolean; - currentStore?: TCreateModalStoreTypes; + storeType?: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; enableQuickIssueCreate: boolean; quickAddCallback?: ( @@ -192,8 +194,8 @@ export const KanBanSwimLanes: React.FC = observer((props) => { group_by, handleIssues, quickActions, - kanBanToggle, - handleKanBanToggle, + kanbanFilters, + handleKanbanFilters, showEmptyGroup, isDragStarted, disableIssueCreation, @@ -227,8 +229,8 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issueIds={issueIds} group_by={group_by} sub_group_by={sub_group_by} - kanBanToggle={kanBanToggle} - handleKanBanToggle={handleKanBanToggle} + kanbanFilters={kanbanFilters} + handleKanbanFilters={handleKanbanFilters} list={groupByList} />
@@ -243,8 +245,8 @@ export const KanBanSwimLanes: React.FC = observer((props) => { sub_group_by={sub_group_by} handleIssues={handleIssues} quickActions={quickActions} - kanBanToggle={kanBanToggle} - handleKanBanToggle={handleKanBanToggle} + kanbanFilters={kanbanFilters} + handleKanbanFilters={handleKanbanFilters} showEmptyGroup={showEmptyGroup} isDragStarted={isDragStarted} disableIssueCreation={disableIssueCreation} diff --git a/web/components/issues/issue-layouts/kanban/utils.ts b/web/components/issues/issue-layouts/kanban/utils.ts index e12fc2477..5c5de8c45 100644 --- a/web/components/issues/issue-layouts/kanban/utils.ts +++ b/web/components/issues/issue-layouts/kanban/utils.ts @@ -8,6 +8,41 @@ import { IProjectViewIssues } from "store/issue/project-views"; import { IWorkspaceIssues } from "store/issue/workspace"; import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues } from "@plane/types"; +const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => { + const sortOrderDefaultValue = 65535; + let currentIssueState = {}; + + if (destinationIssues && destinationIssues.length > 0) { + if (destinationIndex === 0) { + const destinationIssueId = destinationIssues[destinationIndex]; + currentIssueState = { + ...currentIssueState, + sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue, + }; + } else if (destinationIndex === destinationIssues.length) { + const destinationIssueId = destinationIssues[destinationIndex - 1]; + currentIssueState = { + ...currentIssueState, + sort_order: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue, + }; + } else { + const destinationTopIssueId = destinationIssues[destinationIndex - 1]; + const destinationBottomIssueId = destinationIssues[destinationIndex]; + currentIssueState = { + ...currentIssueState, + sort_order: (issueMap[destinationTopIssueId].sort_order + issueMap[destinationBottomIssueId].sort_order) / 2, + }; + } + } else { + currentIssueState = { + ...currentIssueState, + sort_order: sortOrderDefaultValue, + }; + } + + return currentIssueState; +}; + export const handleDragDrop = async ( source: DraggableLocation | null | undefined, destination: DraggableLocation | null | undefined, @@ -50,7 +85,7 @@ export const handleDragDrop = async ( !sourceGroupByColumnId || !destinationGroupByColumnId || !sourceSubGroupByColumnId || - !sourceGroupByColumnId + !destinationSubGroupByColumnId ) return; @@ -76,92 +111,49 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); const removedIssueDetail = issueMap[removed]; + updateIssue = { + id: removedIssueDetail?.id, + project_id: removedIssueDetail?.project_id, + }; + + // for both horizontal and vertical dnd + updateIssue = { + ...updateIssue, + ...handleSortOrder(destinationIssues, destination.index, issueMap), + }; + if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) { - updateIssue = { - id: removedIssueDetail?.id, - }; - - // for both horizontal and vertical dnd - updateIssue = { - ...updateIssue, - ...handleSortOrder(destinationIssues, destination.index, issueMap), - }; - if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) { if (sourceGroupByColumnId != destinationGroupByColumnId) { - if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId }; + if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId }; if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId }; } } else { if (subGroupBy === "state") updateIssue = { ...updateIssue, - state: destinationSubGroupByColumnId, + state_id: destinationSubGroupByColumnId, priority: destinationGroupByColumnId, }; if (subGroupBy === "priority") updateIssue = { ...updateIssue, - state: destinationGroupByColumnId, + state_id: destinationGroupByColumnId, priority: destinationSubGroupByColumnId, }; } } else { - updateIssue = { - id: removedIssueDetail?.id, - }; - - // for both horizontal and vertical dnd - updateIssue = { - ...updateIssue, - ...handleSortOrder(destinationIssues, destination.index, issueMap), - }; - // for horizontal dnd if (sourceColumnId != destinationColumnId) { - if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId }; + if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId }; if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId }; } } if (updateIssue && updateIssue?.id) { - if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); //, viewId); - else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); + if (viewId) + return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue, viewId); + else return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue); } } }; - -const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => { - const sortOrderDefaultValue = 65535; - let currentIssueState = {}; - - if (destinationIssues && destinationIssues.length > 0) { - if (destinationIndex === 0) { - const destinationIssueId = destinationIssues[destinationIndex]; - currentIssueState = { - ...currentIssueState, - sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue, - }; - } else if (destinationIndex === destinationIssues.length) { - const destinationIssueId = destinationIssues[destinationIndex - 1]; - currentIssueState = { - ...currentIssueState, - sort_order: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue, - }; - } else { - const destinationTopIssueId = destinationIssues[destinationIndex - 1]; - const destinationBottomIssueId = destinationIssues[destinationIndex]; - currentIssueState = { - ...currentIssueState, - sort_order: (issueMap[destinationTopIssueId].sort_order + issueMap[destinationBottomIssueId].sort_order) / 2, - }; - } - } else { - currentIssueState = { - ...currentIssueState, - sort_order: sortOrderDefaultValue, - }; - } - - return currentIssueState; -}; diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index dc92aa7d2..b718269b6 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -48,7 +48,7 @@ interface IBaseListRoot { [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; }; viewId?: string; - currentStore: TCreateModalStoreTypes; + storeType: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; } @@ -60,7 +60,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { QuickActions, issueActions, viewId, - currentStore, + storeType, addIssuesToView, canEditPropertiesBasedOnProject, } = props; @@ -134,19 +134,10 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { enableIssueQuickAdd={!!enableQuickAdd} canEditProperties={canEditProperties} disableIssueCreation={!enableIssueCreation || !isEditingAllowed} - currentStore={currentStore} + storeType={storeType} addIssuesToView={addIssuesToView} />
- - {/* {workspaceSlug && peekIssueId && peekProjectId && ( - await handleIssues(issueToUpdate as TIssue, EIssueActions.UPDATE)} - /> - )} */} ); }); diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 7c49e744c..820f98fdd 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -1,13 +1,13 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // components import { IssueProperties } from "../properties/all-properties"; +// hooks +import { useApplication, useIssueDetail, useProject } from "hooks/store"; // ui -import { Spinner, Tooltip } from "@plane/ui"; +import { Spinner, Tooltip, ControlLink } from "@plane/ui"; // types import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; import { EIssueActions } from "../types"; -import { useProject } from "hooks/store"; interface IssueBlockProps { issueId: string; @@ -20,27 +20,29 @@ interface IssueBlockProps { export const IssueBlock: React.FC = observer((props: IssueBlockProps) => { const { issuesMap, issueId, handleIssues, quickActions, displayProperties, canEditProperties } = props; - // router - const router = useRouter(); + // hooks + const { + router: { workspaceSlug, projectId }, + } = useApplication(); + const { getProjectById } = useProject(); + const { setPeekIssue } = useIssueDetail(); + const updateIssue = (issueToUpdate: TIssue) => { handleIssues(issueToUpdate, EIssueActions.UPDATE); }; + const handleIssuePeekOverview = (issue: TIssue) => + workspaceSlug && + issue && + issue.project_id && + issue.id && + setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); + const issue = issuesMap[issueId]; if (!issue) return null; - const handleIssuePeekOverview = () => { - const { query } = router; - - router.push({ - pathname: router.pathname, - query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id }, - }); - }; - const canEditIssueProperties = canEditProperties(issue.project_id); - const { getProjectById } = useProject(); const projectDetails = getProjectById(issue.project_id); return ( @@ -55,14 +57,17 @@ export const IssueBlock: React.FC = observer((props: IssueBlock {issue?.tempId !== undefined && (
)} - -
- {issue.name} -
-
+ + handleIssuePeekOverview(issue)} + className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + > + + {issue.name} + +
{!issue?.tempId ? ( diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 9bf7cfc78..df9103817 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -21,7 +21,6 @@ export interface IGroupByList { issueIds: TGroupedIssues | TUnGroupedIssues | any; issuesMap: TIssueMap; group_by: string | null; - is_list?: boolean; handleIssues: (issue: TIssue, action: EIssueActions) => Promise; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; @@ -35,7 +34,7 @@ export interface IGroupByList { viewId?: string ) => Promise; disableIssueCreation?: boolean; - currentStore: TCreateModalStoreTypes; + storeType: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; } @@ -45,7 +44,6 @@ const GroupByList: React.FC = (props) => { issueIds, issuesMap, group_by, - is_list = false, handleIssues, quickActions, displayProperties, @@ -55,7 +53,7 @@ const GroupByList: React.FC = (props) => { quickAddCallback, viewId, disableIssueCreation, - currentStore, + storeType, addIssuesToView, } = props; // store hooks @@ -70,11 +68,27 @@ const GroupByList: React.FC = (props) => { const prePopulateQuickAddData = (groupByKey: string | null, value: any) => { const defaultState = projectState.projectStates?.find((state) => state.default); - if (groupByKey === null) return { state_id: defaultState?.id }; - else { - if (groupByKey === "state") return { state: groupByKey === "state" ? value : defaultState?.id }; - else return { state_id: defaultState?.id, [groupByKey]: value }; + let preloadedData: object = { state_id: defaultState?.id }; + + if (groupByKey === null) { + preloadedData = { ...preloadedData }; + } else { + if (groupByKey === "state") { + preloadedData = { ...preloadedData, state_id: value }; + } else if (groupByKey === "priority") { + preloadedData = { ...preloadedData, priority: value }; + } else if (groupByKey === "labels" && value != "None") { + preloadedData = { ...preloadedData, label_ids: [value] }; + } else if (groupByKey === "assignees" && value != "None") { + preloadedData = { ...preloadedData, assignee_ids: [value] }; + } else if (groupByKey === "created_by") { + preloadedData = { ...preloadedData }; + } else { + preloadedData = { ...preloadedData, [groupByKey]: value }; + } } + + return preloadedData; }; const validateEmptyIssueGroups = (issues: TIssue[]) => { @@ -83,6 +97,10 @@ const GroupByList: React.FC = (props) => { return true; }; + const is_list = group_by === null ? true : false; + + const isGroupByCreatedBy = group_by === "created_by"; + return (
{list && @@ -97,8 +115,8 @@ const GroupByList: React.FC = (props) => { title={_list.name || ""} count={is_list ? issueIds?.length || 0 : issueIds?.[_list.id]?.length || 0} issuePayload={_list.payload} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} + disableIssueCreation={disableIssueCreation || isGroupByCreatedBy} + storeType={storeType} addIssuesToView={addIssuesToView} />
@@ -114,7 +132,7 @@ const GroupByList: React.FC = (props) => { /> )} - {enableIssueQuickAdd && !disableIssueCreation && ( + {enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && (
Promise; viewId?: string; disableIssueCreation?: boolean; - currentStore: TCreateModalStoreTypes; + storeType: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; } @@ -166,7 +184,7 @@ export const List: React.FC = (props) => { enableIssueQuickAdd, canEditProperties, disableIssueCreation, - currentStore, + storeType, addIssuesToView, } = props; @@ -185,7 +203,7 @@ export const List: React.FC = (props) => { quickAddCallback={quickAddCallback} viewId={viewId} disableIssueCreation={disableIssueCreation} - currentStore={currentStore} + storeType={storeType} addIssuesToView={addIssuesToView} />
diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index 4989cb250..8e7b0ace3 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -19,12 +19,12 @@ interface IHeaderGroupByCard { count: number; issuePayload: Partial; disableIssueCreation?: boolean; - currentStore: TCreateModalStoreTypes; + storeType: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; } export const HeaderGroupByCard = observer( - ({ icon, title, count, issuePayload, disableIssueCreation, currentStore, addIssuesToView }: IHeaderGroupByCard) => { + ({ icon, title, count, issuePayload, disableIssueCreation, storeType, addIssuesToView }: IHeaderGroupByCard) => { const router = useRouter(); const { workspaceSlug, projectId, moduleId, cycleId } = router.query; @@ -101,7 +101,12 @@ export const HeaderGroupByCard = observer( fieldsToShow={["all"]} /> ) : ( - setIsOpen(false)} data={issuePayload} /> + setIsOpen(false)} + data={issuePayload} + storeType={storeType} + /> )} {renderExistingIssueModal && ( diff --git a/web/components/issues/issue-layouts/list/list-view-types.d.ts b/web/components/issues/issue-layouts/list/list-view-types.d.ts index 674ae92d1..9e3bb8701 100644 --- a/web/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/components/issues/issue-layouts/list/list-view-types.d.ts @@ -4,4 +4,5 @@ export interface IQuickActionProps { handleUpdate?: (data: TIssue) => Promise; handleRemoveFromView?: () => Promise; customActionButton?: React.ReactElement; + portalElement?: HTMLDivElement | null; } diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx index b6e39606e..540d4d7f6 100644 --- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -4,7 +4,7 @@ import { useForm } from "react-hook-form"; import { PlusIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks -import { useProject, useWorkspace } from "hooks/store"; +import { useProject } from "hooks/store"; import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -62,9 +62,10 @@ export const ListQuickAddIssueForm: FC = observer((props // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // store hooks - const { currentWorkspace } = useWorkspace(); - const { currentProjectDetails } = useProject(); + // hooks + const { getProjectById } = useProject(); + + const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined; const ref = useRef(null); @@ -88,11 +89,11 @@ export const ListQuickAddIssueForm: FC = observer((props }, [isOpen, reset]); const onSubmitHandler = async (formData: TIssue) => { - if (isSubmitting || !currentWorkspace || !currentProjectDetails || !workspaceSlug || !projectId) return; + if (isSubmitting || !workspaceSlug || !projectId) return; reset({ ...defaultValues }); - const payload = createIssuePayload(currentWorkspace, currentProjectDetails, { + const payload = createIssuePayload(projectId.toString(), { ...(prePopulatedData ?? {}), ...formData, }); @@ -127,12 +128,7 @@ export const ListQuickAddIssueForm: FC = observer((props onSubmit={handleSubmit(onSubmitHandler)} className="flex w-full items-center gap-x-3 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-3" > - +
{`Press 'Enter' to add another issue`}
diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index 388699dc7..e29c7dbb0 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -34,7 +34,7 @@ export const ArchivedIssueListLayout: FC = observer(() => { issues={issues} QuickActions={ArchivedIssueQuickActions} issueActions={issueActions} - currentStore={EIssuesStoreType.PROJECT} + storeType={EIssuesStoreType.PROJECT} /> ); }); diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index c1db51411..89da8dd54 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -48,7 +48,7 @@ export const CycleListLayout: React.FC = observer(() => { QuickActions={CycleIssueQuickActions} issueActions={issueActions} viewId={cycleId?.toString()} - currentStore={EIssuesStoreType.CYCLE} + storeType={EIssuesStoreType.CYCLE} addIssuesToView={(issueIds: string[]) => { if (!workspaceSlug || !projectId || !cycleId) throw new Error(); return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); diff --git a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx index ef1edc831..e11971874 100644 --- a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx @@ -43,7 +43,7 @@ export const DraftIssueListLayout: FC = observer(() => { issues={issues} QuickActions={ProjectIssueQuickActions} issueActions={issueActions} - currentStore={EIssuesStoreType.PROJECT} + storeType={EIssuesStoreType.PROJECT} /> ); }); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 947cfe55b..fb874b8f6 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -25,12 +25,12 @@ export const ModuleListLayout: React.FC = observer(() => { [EIssueActions.UPDATE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); }, [EIssueActions.DELETE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); }, [EIssueActions.REMOVE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; @@ -48,7 +48,7 @@ export const ModuleListLayout: React.FC = observer(() => { QuickActions={ModuleIssueQuickActions} issueActions={issueActions} viewId={moduleId?.toString()} - currentStore={EIssuesStoreType.MODULE} + storeType={EIssuesStoreType.MODULE} addIssuesToView={(issueIds: string[]) => { if (!workspaceSlug || !projectId || !moduleId) throw new Error(); return issues.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds); diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index 55db4cd71..1e7e364d5 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -52,7 +52,7 @@ export const ProfileIssuesListLayout: FC = observer(() => { issues={issues} QuickActions={ProjectIssueQuickActions} issueActions={issueActions} - currentStore={EIssuesStoreType.PROFILE} + storeType={EIssuesStoreType.PROFILE} canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject} /> ); diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index b99b431c8..f0479b71f 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -44,7 +44,7 @@ export const ListLayout: FC = observer(() => { issues={issues} QuickActions={ProjectIssueQuickActions} issueActions={issueActions} - currentStore={EIssuesStoreType.PROJECT} + storeType={EIssuesStoreType.PROJECT} /> ); }); diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index 8139307e6..dd384ba93 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -1,50 +1,43 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // store import { useIssues } from "hooks/store"; // constants -import { useRouter } from "next/router"; +import { EIssuesStoreType } from "constants/issue"; +// types import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; // components import { BaseListRoot } from "../base-list-root"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EIssuesStoreType } from "constants/issue"; -export interface IViewListLayout {} +export interface IViewListLayout { + issueActions: { + [EIssueActions.DELETE]: (issue: TIssue) => Promise; + [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; + [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + }; +} -export const ProjectViewListLayout: React.FC = observer(() => { +export const ProjectViewListLayout: React.FC = observer((props) => { + const { issueActions } = props; // store const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT_VIEW); const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId, viewId } = router.query; if (!workspaceSlug || !projectId) return null; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - }), - [issues, workspaceSlug, projectId] - ); - return ( ); }); diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index fe05d834b..e258c96fc 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react-lite"; import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react"; // hooks -import { useLabel } from "hooks/store"; +import { useEstimate, useLabel } from "hooks/store"; // components import { IssuePropertyLabels } from "../properties/labels"; import { Tooltip } from "@plane/ui"; @@ -29,6 +29,7 @@ export interface IIssueProperties { export const IssueProperties: React.FC = observer((props) => { const { issue, handleIssues, displayProperties, isReadOnly, className } = props; const { labelMap } = useLabel(); + const { areEstimatesEnabledForCurrentProject } = useEstimate(); const handleState = (stateId: string) => { handleIssues({ ...issue, state_id: stateId }); @@ -92,7 +93,6 @@ export const IssueProperties: React.FC = observer((props) => { {/* label */} - = observer((props) => {
} @@ -141,24 +142,26 @@ export const IssueProperties: React.FC = observer((props) => { onChange={handleAssignee} disabled={isReadOnly} multiple - buttonVariant={issue.assignee_ids.length > 0 ? "transparent-without-text" : "border-without-text"} - buttonClassName={issue.assignee_ids.length > 0 ? "hover:bg-transparent px-0" : ""} + buttonVariant={issue.assignee_ids?.length > 0 ? "transparent-without-text" : "border-without-text"} + buttonClassName={issue.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""} />
{/* estimates */} - -
- -
-
+ {areEstimatesEnabledForCurrentProject && ( + +
+ +
+
+ )} {/* extra render properties */} {/* sub-issues */} diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index b22083c07..67295af0f 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -62,6 +62,18 @@ export const IssuePropertyLabels: React.FC = observer((pro if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false)); }; + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + if (!value) return null; let projectLabels: IIssueLabel[] = defaultOptions; @@ -86,18 +98,6 @@ export const IssuePropertyLabels: React.FC = observer((pro const filteredOptions = query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - const label = (
{value.length > 0 ? ( diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index efd9490d7..92964ec97 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -11,9 +11,11 @@ import { copyUrlToClipboard } from "helpers/string.helper"; // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; +// constants +import { EIssuesStoreType } from "constants/issue"; export const AllIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, customActionButton } = props; + const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props; // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); @@ -58,12 +60,17 @@ export const AllIssueQuickActions: React.FC = (props) => { onSubmit={async (data) => { if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} + storeType={EIssuesStoreType.PROJECT} /> - + { - e.preventDefault(); - e.stopPropagation(); + onClick={() => { handleCopyIssueLink(); }} > @@ -73,9 +80,7 @@ export const AllIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setIssueToEdit(issue); setCreateUpdateIssueModal(true); }} @@ -86,9 +91,7 @@ export const AllIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setCreateUpdateIssueModal(true); }} > @@ -98,9 +101,7 @@ export const AllIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setDeleteIssueModal(true); }} > diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index 8d6735277..264093778 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -12,7 +12,7 @@ import { copyUrlToClipboard } from "helpers/string.helper"; import { IQuickActionProps } from "../list/list-view-types"; export const ArchivedIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, customActionButton } = props; + const { issue, handleDelete, customActionButton, portalElement } = props; const router = useRouter(); const { workspaceSlug } = router.query; @@ -40,11 +40,15 @@ export const ArchivedIssueQuickActions: React.FC = (props) => handleClose={() => setDeleteIssueModal(false)} onSubmit={handleDelete} /> - + { - e.preventDefault(); - e.stopPropagation(); + onClick={() => { handleCopyIssueLink(); }} > @@ -54,9 +58,7 @@ export const ArchivedIssueQuickActions: React.FC = (props) =>
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setDeleteIssueModal(true); }} > diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 6d7e08152..d21535639 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -11,9 +11,11 @@ import { copyUrlToClipboard } from "helpers/string.helper"; // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; +// constants +import { EIssuesStoreType } from "constants/issue"; export const CycleIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props; + const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props; // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); @@ -58,12 +60,17 @@ export const CycleIssueQuickActions: React.FC = (props) => { onSubmit={async (data) => { if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} + storeType={EIssuesStoreType.CYCLE} /> - + { - e.preventDefault(); - e.stopPropagation(); + onClick={() => { handleCopyIssueLink(); }} > @@ -73,9 +80,7 @@ export const CycleIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setIssueToEdit({ ...issue, cycle: cycleId?.toString() ?? null, @@ -89,9 +94,7 @@ export const CycleIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { handleRemoveFromView && handleRemoveFromView(); }} > @@ -101,9 +104,7 @@ export const CycleIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setCreateUpdateIssueModal(true); }} > @@ -113,9 +114,7 @@ export const CycleIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setDeleteIssueModal(true); }} > diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 27d16c781..0decd5555 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -11,9 +11,11 @@ import { copyUrlToClipboard } from "helpers/string.helper"; // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; +// constants +import { EIssuesStoreType } from "constants/issue"; export const ModuleIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props; + const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props; // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); @@ -58,12 +60,17 @@ export const ModuleIssueQuickActions: React.FC = (props) => { onSubmit={async (data) => { if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} + storeType={EIssuesStoreType.MODULE} /> - + { - e.preventDefault(); - e.stopPropagation(); + onClick={() => { handleCopyIssueLink(); }} > @@ -73,9 +80,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null }); setCreateUpdateIssueModal(true); }} @@ -86,9 +91,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { handleRemoveFromView && handleRemoveFromView(); }} > @@ -98,9 +101,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => {
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setCreateUpdateIssueModal(true); }} > diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 083a22e35..044030184 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -14,9 +14,10 @@ import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constant import { EUserProjectRoles } from "constants/project"; +import { EIssuesStoreType } from "constants/issue"; export const ProjectIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, customActionButton } = props; + const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -67,12 +68,17 @@ export const ProjectIssueQuickActions: React.FC = (props) => onSubmit={async (data) => { if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} + storeType={EIssuesStoreType.PROJECT} /> - + { - e.preventDefault(); - e.stopPropagation(); + onClick={() => { handleCopyIssueLink(); }} > @@ -84,9 +90,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => {isEditingAllowed && ( <> { - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setIssueToEdit(issue); setCreateUpdateIssueModal(true); }} @@ -97,9 +101,7 @@ export const ProjectIssueQuickActions: React.FC = (props) =>
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setCreateUpdateIssueModal(true); }} > @@ -109,9 +111,7 @@ export const ProjectIssueQuickActions: React.FC = (props) =>
{ - e.preventDefault(); - e.stopPropagation(); + onClick={() => { setDeleteIssueModal(true); }} > diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 0b4e334c7..b87bec2d0 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // hooks -import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store"; +import { useGlobalView, useIssues, useUser } from "hooks/store"; // components import { GlobalViewsAppliedFiltersRoot } from "components/issues"; import { SpreadsheetView } from "components/issues/issue-layouts"; @@ -16,46 +16,43 @@ import { EIssueActions } from "../types"; import { EUserProjectRoles } from "constants/project"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -type Props = { - type?: TStaticViewTypes | null; -}; -export const AllIssueLayoutRoot: React.FC = observer((props) => { - const { type = null } = props; + +export const AllIssueLayoutRoot: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query as { workspaceSlug: string; globalViewId: string }; + const { workspaceSlug, globalViewId } = router.query; // store const { - issuesFilter: { issueFilters, fetchFilters, updateFilters }, + issuesFilter: { filters, fetchFilters, updateFilters }, issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue }, - issueMap, } = useIssues(EIssuesStoreType.GLOBAL); + const { dataViewId, issueIds } = groupedIssueIds; const { membership: { currentWorkspaceAllProjectsRole }, } = useUser(); const { fetchAllGlobalViews } = useGlobalView(); - const { - workspace: { workspaceLabels }, - } = useLabel(); // derived values - const currentIssueView = type ?? globalViewId; useSWR(workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS${workspaceSlug}` : null, async () => { if (workspaceSlug) { - await fetchAllGlobalViews(workspaceSlug); + await fetchAllGlobalViews(workspaceSlug.toString()); } }); useSWR( - workspaceSlug && currentIssueView ? `WORKSPACE_GLOBAL_VIEW_ISSUES_${workspaceSlug}_${currentIssueView}` : null, + workspaceSlug && globalViewId ? `WORKSPACE_GLOBAL_VIEW_ISSUES_${workspaceSlug}_${globalViewId}` : null, async () => { - if (workspaceSlug && currentIssueView) { - await fetchAllGlobalViews(workspaceSlug); - await fetchFilters(workspaceSlug, currentIssueView); - await fetchIssues(workspaceSlug, currentIssueView, groupedIssueIds ? "mutation" : "init-loader"); + if (workspaceSlug && globalViewId) { + await fetchAllGlobalViews(workspaceSlug.toString()); + await fetchFilters(workspaceSlug.toString(), globalViewId.toString()); + await fetchIssues( + workspaceSlug.toString(), + globalViewId.toString(), + groupedIssueIds ? "mutation" : "init-loader" + ); } } ); @@ -68,22 +65,21 @@ export const AllIssueLayoutRoot: React.FC = observer((props) => { return !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; }; - const issueIds = (groupedIssueIds ?? []) as TUnGroupedIssues; - const issuesArray = issueIds?.filter((id) => id && issueMap?.[id]).map((id) => issueMap?.[id]); + const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; const issueActions = useMemo( () => ({ [EIssueActions.UPDATE]: async (issue: TIssue) => { const projectId = issue.project_id; - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !globalViewId) return; - await updateIssue(workspaceSlug, projectId, issue.id, issue, currentIssueView); + await updateIssue(workspaceSlug.toString(), projectId, issue.id, issue, globalViewId.toString()); }, [EIssueActions.DELETE]: async (issue: TIssue) => { const projectId = issue.project_id; - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !globalViewId) return; - await removeIssue(workspaceSlug, projectId, issue.id, currentIssueView); + await removeIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString()); }, }), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -103,22 +99,22 @@ export const AllIssueLayoutRoot: React.FC = observer((props) => { (updatedDisplayFilter: Partial) => { if (!workspaceSlug) return; - updateFilters(workspaceSlug, undefined, EIssueFilterType.DISPLAY_FILTERS, { ...updatedDisplayFilter }); + updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_FILTERS, { ...updatedDisplayFilter }); }, [updateFilters, workspaceSlug] ); return (
- {globalViewId != currentIssueView && (loader === "init-loader" || !groupedIssueIds) ? ( + {!globalViewId || globalViewId !== dataViewId || loader === "init-loader" || !issueIds ? (
) : ( <> - + - {(groupedIssueIds ?? {}).length == 0 ? ( + {(issueIds ?? {}).length == 0 ? ( <>{/* */} ) : (
@@ -126,7 +122,7 @@ export const AllIssueLayoutRoot: React.FC = observer((props) => { displayProperties={issueFilters?.displayProperties ?? {}} displayFilters={issueFilters?.displayFilters ?? {}} handleDisplayFilterUpdate={handleDisplayFiltersUpdate} - issues={issuesArray} + issueIds={issueIds} quickActions={(issue) => ( = observer((props) => { handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)} /> )} - labels={workspaceLabels || undefined} handleIssues={handleIssues} canEditProperties={canEditProperties} - viewId={currentIssueView} + viewId={globalViewId} />
)} diff --git a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx index 3b23351d9..bcd30555e 100644 --- a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx @@ -5,31 +5,53 @@ import useSWR from "swr"; // mobx store import { useIssues } from "hooks/store"; // components -import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot } from "components/issues"; +import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot, ProjectEmptyState } from "components/issues"; import { EIssuesStoreType } from "constants/issue"; +// ui +import { Spinner } from "@plane/ui"; export const ArchivedIssueLayoutRoot: React.FC = observer(() => { + // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = router.query; + // hooks + const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); - const { - issues: { groupedIssueIds, fetchIssues }, - issuesFilter: { fetchFilters }, - } = useIssues(EIssuesStoreType.ARCHIVED); - - useSWR(workspaceSlug && projectId ? `ARCHIVED_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => { - if (workspaceSlug && projectId) { - await fetchFilters(workspaceSlug, projectId); - await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader"); + useSWR( + workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null, + async () => { + if (workspaceSlug && projectId) { + await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString()); + await issues?.fetchIssues( + workspaceSlug.toString(), + projectId.toString(), + issues?.groupedIssueIds ? "mutation" : "init-loader" + ); + } } - }); + ); + if (!workspaceSlug || !projectId) return <>; return (
-
- -
+ + {issues?.loader === "init-loader" ? ( +
+ +
+ ) : ( + <> + {!issues?.groupedIssueIds ? ( + // TODO: Replace this with project view empty state + + ) : ( +
+ +
+ )} + + )}
); }); diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index bc63a8aaf..0f32e71c2 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -13,6 +13,7 @@ import { CycleKanBanLayout, CycleListLayout, CycleSpreadsheetLayout, + IssuePeekOverview, } from "components/issues"; import { TransferIssues, TransferIssuesModal } from "components/cycles"; // ui @@ -21,36 +22,37 @@ import { Spinner } from "@plane/ui"; import { EIssuesStoreType } from "constants/issue"; export const CycleLayoutRoot: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug, projectId, cycleId } = router.query; + // store hooks + const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { getCycleById } = useCycle(); + // state const [transferIssuesModal, setTransferIssuesModal] = useState(false); - const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query as { - workspaceSlug: string; - projectId: string; - cycleId: string; - }; - // store hooks - const { - issues: { loader, groupedIssueIds, fetchIssues }, - issuesFilter: { issueFilters, fetchFilters }, - } = useIssues(EIssuesStoreType.CYCLE); - const { getCycleById } = useCycle(); - useSWR( - workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null, + workspaceSlug && projectId && cycleId + ? `CYCLE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${cycleId.toString()}` + : null, async () => { if (workspaceSlug && projectId && cycleId) { - await fetchFilters(workspaceSlug, projectId, cycleId); - await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader", cycleId); + await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); + await issues?.fetchIssues( + workspaceSlug.toString(), + projectId.toString(), + issues?.groupedIssueIds ? "mutation" : "init-loader", + cycleId.toString() + ); } } ); - const activeLayout = issueFilters?.displayFilters?.layout; + const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const cycleDetails = cycleId ? getCycleById(cycleId) : undefined; - const cycleStatus = cycleDetails?.status.toLocaleLowerCase() ?? "draft"; + const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; + const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft"; + if (!workspaceSlug || !projectId || !cycleId) return <>; return ( <> setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> @@ -59,28 +61,36 @@ export const CycleLayoutRoot: React.FC = observer(() => { {cycleStatus === "completed" && setTransferIssuesModal(true)} />} - {loader === "init-loader" || !groupedIssueIds ? ( + {issues?.loader === "init-loader" ? (
) : ( <> - {Object.keys(groupedIssueIds ?? {}).length == 0 ? ( - + {!issues?.groupedIssueIds ? ( + ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
+ <> +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ {/* peek overview */} + + )} )} diff --git a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx index 79d4b0ac9..19a4da553 100644 --- a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx @@ -2,49 +2,64 @@ import React from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; -// mobx store +// hooks import { useIssues } from "hooks/store"; +// components import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue"; import { DraftIssueListLayout } from "../list/roots/draft-issue-root"; +import { ProjectEmptyState } from "../empty-states"; +// ui import { Spinner } from "@plane/ui"; import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root"; +// constants import { EIssuesStoreType } from "constants/issue"; export const DraftIssueLayoutRoot: React.FC = observer(() => { + // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = router.query; + // hooks + const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - const { - issues: { loader, groupedIssueIds, fetchIssues }, - issuesFilter: { issueFilters, fetchFilters }, - } = useIssues(EIssuesStoreType.DRAFT); - - useSWR(workspaceSlug && projectId ? `DRAFT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => { - if (workspaceSlug && projectId) { - await fetchFilters(workspaceSlug, projectId); - await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader"); + useSWR( + workspaceSlug && projectId ? `DRAFT_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null, + async () => { + if (workspaceSlug && projectId) { + await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString()); + await issues?.fetchIssues( + workspaceSlug.toString(), + projectId.toString(), + issues?.groupedIssueIds ? "mutation" : "init-loader" + ); + } } - }); + ); - const activeLayout = issueFilters?.displayFilters?.layout || undefined; + const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined; + if (!workspaceSlug || !projectId) return <>; return (
- {loader === "init-loader" ? ( + {issues?.loader === "init-loader" ? (
) : ( <> -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : null} -
+ {!issues?.groupedIssueIds ? ( + // TODO: Replace this with project view empty state + + ) : ( +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : null} +
+ )} )}
diff --git a/web/components/issues/issue-layouts/roots/index.ts b/web/components/issues/issue-layouts/roots/index.ts index 72f71aae2..727e3e393 100644 --- a/web/components/issues/issue-layouts/roots/index.ts +++ b/web/components/issues/issue-layouts/roots/index.ts @@ -1,6 +1,7 @@ -export * from "./cycle-layout-root"; -export * from "./all-issue-layout-root"; -export * from "./module-layout-root"; export * from "./project-layout-root"; +export * from "./module-layout-root"; +export * from "./cycle-layout-root"; export * from "./project-view-layout-root"; export * from "./archived-issue-layout-root"; +export * from "./draft-issue-layout-root"; +export * from "./all-issue-layout-root"; diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index b14d3cb2f..808cad91b 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -2,11 +2,11 @@ import React from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; - // mobx store import { useIssues } from "hooks/store"; // components import { + IssuePeekOverview, ModuleAppliedFiltersRoot, ModuleCalendarLayout, ModuleEmptyState, @@ -17,61 +17,71 @@ import { } from "components/issues"; // ui import { Spinner } from "@plane/ui"; +// constants import { EIssuesStoreType } from "constants/issue"; export const ModuleLayoutRoot: React.FC = observer(() => { + // router const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; - - const { - issues: { loader, groupedIssueIds, fetchIssues }, - issuesFilter: { issueFilters, fetchFilters }, - } = useIssues(EIssuesStoreType.MODULE); + const { workspaceSlug, projectId, moduleId } = router.query; + // hooks + const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); useSWR( - workspaceSlug && projectId && moduleId ? `MODULE_ISSUES_V3_${workspaceSlug}_${projectId}_${moduleId}` : null, + workspaceSlug && projectId && moduleId + ? `MODULE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${moduleId.toString()}` + : null, async () => { if (workspaceSlug && projectId && moduleId) { - await fetchFilters(workspaceSlug, projectId, moduleId); - await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader", moduleId); + await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); + await issues?.fetchIssues( + workspaceSlug.toString(), + projectId.toString(), + issues?.groupedIssueIds ? "mutation" : "init-loader", + moduleId.toString() + ); } } ); - const activeLayout = issueFilters?.displayFilters?.layout || undefined; + const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined; + if (!workspaceSlug || !projectId || !moduleId) return <>; return (
- {loader === "init-loader" || !groupedIssueIds ? ( + {issues?.loader === "init-loader" ? (
) : ( <> - {Object.keys(groupedIssueIds ?? {}).length == 0 ? ( - + {!issues?.groupedIssueIds ? ( + ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
+ <> +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ {/* peek overview */} + + )} - {/* */} )}
diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index f8e428e5c..1edba5563 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -1,3 +1,4 @@ +import { FC } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; @@ -10,58 +11,76 @@ import { ProjectAppliedFiltersRoot, ProjectSpreadsheetLayout, ProjectEmptyState, + IssuePeekOverview, } from "components/issues"; +// ui import { Spinner } from "@plane/ui"; -import { useIssues } from "hooks/store/use-issues"; -import { EIssuesStoreType } from "constants/issue"; // hooks +import { useIssues } from "hooks/store"; +// constants +import { EIssuesStoreType } from "constants/issue"; -export const ProjectLayoutRoot: React.FC = observer(() => { +export const ProjectLayoutRoot: FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - + const { workspaceSlug, projectId } = router.query; + // hooks const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - useSWR( - workspaceSlug && projectId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, - async () => { - if (workspaceSlug && projectId) { - await issuesFilter?.fetchFilters(workspaceSlug, projectId); - await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader"); - } - }, - { revalidateOnFocus: false, refreshInterval: 600000, revalidateOnMount: true } - ); + useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null, async () => { + if (workspaceSlug && projectId) { + await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString()); + await issues?.fetchIssues( + workspaceSlug.toString(), + projectId.toString(), + issues?.groupedIssueIds ? "mutation" : "init-loader" + ); + } + }); const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; + if (!workspaceSlug || !projectId) return <>; return (
- {issues?.loader === "init-loader" || !issues?.groupedIssueIds ? ( + {issues?.loader === "init-loader" ? (
) : ( <> - {(issues?.groupedIssueIds ?? {}).length == 0 ? ( - - ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} + {!issues?.groupedIssueIds ? ( +
+
+ ) : ( + <> +
+ {/* mutation loader */} + {issues?.loader === "mutation" && ( +
+ +
+ )} + + {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ + {/* peek overview */} + + )} )} diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index 0c2b323a2..d3d4f0417 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; @@ -6,61 +6,95 @@ import useSWR from "swr"; import { useIssues } from "hooks/store"; // components import { + IssuePeekOverview, ProjectViewAppliedFiltersRoot, ProjectViewCalendarLayout, + ProjectViewEmptyState, ProjectViewGanttLayout, ProjectViewKanBanLayout, ProjectViewListLayout, ProjectViewSpreadsheetLayout, } from "components/issues"; import { Spinner } from "@plane/ui"; +// constants import { EIssuesStoreType } from "constants/issue"; +// types +import { TIssue } from "@plane/types"; +import { EIssueActions } from "../types"; export const ProjectViewLayoutRoot: React.FC = observer(() => { + // router const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query as { - workspaceSlug: string; - projectId: string; - viewId?: string; - }; + const { workspaceSlug, projectId, viewId } = router.query; + // hooks + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); - const { - issues: { loader, groupedIssueIds, fetchIssues }, - issuesFilter: { issueFilters, fetchFilters }, - } = useIssues(EIssuesStoreType.PROJECT_VIEW); - - useSWR(workspaceSlug && projectId && viewId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => { - if (workspaceSlug && projectId && viewId) { - await fetchFilters(workspaceSlug, projectId, viewId); - await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader"); + useSWR( + workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}_${viewId}` : null, + async () => { + if (workspaceSlug && projectId && viewId) { + await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), viewId.toString()); + await issues?.fetchIssues( + workspaceSlug.toString(), + projectId.toString(), + issues?.groupedIssueIds ? "mutation" : "init-loader", + viewId.toString() + ); + } } - }); + ); - const activeLayout = issueFilters?.displayFilters?.layout; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; + await issues.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue, viewId?.toString()); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; + + await issues.removeIssue(workspaceSlug.toString(), projectId.toString(), issue.id, viewId?.toString()); + }, + }), + [issues, workspaceSlug, projectId, viewId] + ); + + const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; + + if (!workspaceSlug || !projectId || !viewId) return <>; return (
- {loader === "init-loader" ? ( + {issues?.loader === "init-loader" ? (
) : ( <> -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
+ {!issues?.groupedIssueIds ? ( + + ) : ( + <> +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ + {/* peek overview */} + + + )} )}
diff --git a/web/components/issues/issue-layouts/save-filter-view.tsx b/web/components/issues/issue-layouts/save-filter-view.tsx index 42fac26ef..8bf2cb211 100644 --- a/web/components/issues/issue-layouts/save-filter-view.tsx +++ b/web/components/issues/issue-layouts/save-filter-view.tsx @@ -20,7 +20,7 @@ export const SaveFilterView: FC = (props) => { setViewModal(false)} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 543e33aad..3a022d447 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -2,7 +2,7 @@ import { FC, useCallback } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useIssues, useLabel, useProjectState, useUser } from "hooks/store"; +import { useIssues, useUser } from "hooks/store"; // views import { SpreadsheetView } from "./spreadsheet-view"; // types @@ -40,26 +40,24 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { const { membership: { currentProjectRole }, } = useUser(); - const { - project: { projectLabels }, - } = useLabel(); - const { projectStates } = useProjectState(); // derived values const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {}; // user role validation const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - const canEditProperties = (projectId: string | undefined) => { - const isEditingAllowedBasedOnProject = - canEditPropertiesBasedOnProject && projectId ? canEditPropertiesBasedOnProject(projectId) : isEditingAllowed; + const canEditProperties = useCallback( + (projectId: string | undefined) => { + const isEditingAllowedBasedOnProject = + canEditPropertiesBasedOnProject && projectId ? canEditPropertiesBasedOnProject(projectId) : isEditingAllowed; + + return enableInlineEditing && isEditingAllowedBasedOnProject; + }, + [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] + ); - return enableInlineEditing && isEditingAllowedBasedOnProject; - }; const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues; - const issues = issueIds?.filter((id) => id && issueMap?.[id]).map((id) => issueMap?.[id]); - const handleIssues = useCallback( async (issue: TIssue, action: EIssueActions) => { if (issueActions[action]) { @@ -86,27 +84,31 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { [issueFiltersStore, projectId, workspaceSlug, viewId] ); + const renderQuickActions = useCallback( + (issue: TIssue, customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null) => ( + handleIssues(issue, EIssueActions.DELETE)} + handleUpdate={ + issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined + } + handleRemoveFromView={ + issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined + } + portalElement={portalElement} + /> + ), + [handleIssues] + ); + return ( ( - handleIssues(issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined - } - /> - )} - labels={projectLabels ?? []} - states={projectStates} + issueIds={issueIds} + quickActions={renderQuickActions} handleIssues={handleIssues} canEditProperties={canEditProperties} quickAddCallback={issueStore.quickAddIssue} diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx index 6dbcecb8d..2656143ac 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx @@ -1,56 +1,34 @@ import React from "react"; -// hooks -import { useIssueDetail } from "hooks/store"; +import { observer } from "mobx-react-lite"; // components import { ProjectMemberDropdown } from "components/dropdowns"; // types import { TIssue } from "@plane/types"; type Props = { - issueId: string; + issue: TIssue; onChange: (issue: TIssue, data: Partial) => void; - expandedIssues: string[]; disabled: boolean; }; -export const SpreadsheetAssigneeColumn: React.FC = ({ issueId, onChange, expandedIssues, disabled }) => { - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); +export const SpreadsheetAssigneeColumn: React.FC = observer((props: Props) => { + const { issue, onChange, disabled } = props; return ( - <> - {issueDetail && ( -
- onChange(issueDetail, { assignee_ids: data })} - projectId={issueDetail?.project_id} - disabled={disabled} - multiple - placeholder="Assignees" - buttonVariant={issueDetail.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text"} - buttonClassName="text-left" - buttonContainerClassName="w-full" - /> -
- )} - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId) => ( - - ))} - +
+ onChange(issue, { assignee_ids: data })} + projectId={issue?.project_id} + disabled={disabled} + multiple + placeholder="Assignees" + buttonVariant={ + issue?.assignee_ids && issue.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text" + } + buttonClassName="text-left" + buttonContainerClassName="w-full" + /> +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/attachment-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/attachment-column.tsx index 4b4bdbb53..c17a433b8 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/attachment-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/attachment-column.tsx @@ -1,39 +1,18 @@ import React from "react"; -// hooks +import { observer } from "mobx-react-lite"; // types -import { useIssueDetail } from "hooks/store"; +import { TIssue } from "@plane/types"; type Props = { - issueId: string; - expandedIssues: string[]; + issue: TIssue; }; -export const SpreadsheetAttachmentColumn: React.FC = (props) => { - const { issueId, expandedIssues } = props; - - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); - - // const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded); +export const SpreadsheetAttachmentColumn: React.FC = observer((props) => { + const { issue } = props; return ( - <> -
- {issueDetail?.attachment_count} {issueDetail?.attachment_count === 1 ? "attachment" : "attachments"} -
- - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( -
- -
- ))} - +
+ {issue?.attachment_count} {issue?.attachment_count === 1 ? "attachment" : "attachments"} +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/columns-list.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/columns-list.tsx deleted file mode 100644 index e7f046796..000000000 --- a/web/components/issues/issue-layouts/spreadsheet/columns/columns-list.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { observer } from "mobx-react-lite"; -// hooks -import { useProject } from "hooks/store"; -// components -import { SpreadsheetColumn } from "components/issues"; -// types -import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState } from "@plane/types"; - -type Props = { - displayFilters: IIssueDisplayFilterOptions; - displayProperties: IIssueDisplayProperties; - canEditProperties: (projectId: string | undefined) => boolean; - expandedIssues: string[]; - handleDisplayFilterUpdate: (data: Partial) => void; - handleUpdateIssue: (issue: TIssue, data: Partial) => void; - issues: TIssue[] | undefined; - labels?: IIssueLabel[] | undefined; - states?: IState[] | undefined; -}; - -export const SpreadsheetColumnsList: React.FC = observer((props) => { - const { - canEditProperties, - displayFilters, - displayProperties, - expandedIssues, - handleDisplayFilterUpdate, - handleUpdateIssue, - issues, - labels, - states, - } = props; - // store hooks - const { currentProjectDetails } = useProject(); - - const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null; - - return ( - <> - {displayProperties.state && ( - - )} - {displayProperties.priority && ( - - )} - {displayProperties.assignee && ( - - )} - {displayProperties.labels && ( - - )}{" "} - {displayProperties.start_date && ( - - )} - {displayProperties.due_date && ( - - )} - {displayProperties.estimate && isEstimateEnabled && ( - - )} - {displayProperties.created_on && ( - - )} - {displayProperties.updated_on && ( - - )} - {displayProperties.link && ( - - )} - {displayProperties.attachment_count && ( - - )} - {displayProperties.sub_issue_count && ( - - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx index 176b8ea14..8d373efb4 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx @@ -1,38 +1,19 @@ import React from "react"; -// hooks -import { useIssueDetail } from "hooks/store"; +import { observer } from "mobx-react-lite"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; // types +import { TIssue } from "@plane/types"; type Props = { - issueId: string; - expandedIssues: string[]; + issue: TIssue; }; -export const SpreadsheetCreatedOnColumn: React.FC = ({ issueId, expandedIssues }) => { - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); - +export const SpreadsheetCreatedOnColumn: React.FC = observer((props: Props) => { + const { issue } = props; return ( - <> - {issueDetail && ( -
- {renderFormattedDate(issueDetail.created_at)} -
- )} - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( -
- -
- ))} - +
+ {renderFormattedDate(issue.created_at)} +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index 32c871b90..dbc27a3db 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx @@ -1,6 +1,5 @@ import React from "react"; -// hooks -import { useIssueDetail } from "hooks/store"; +import { observer } from "mobx-react-lite"; // components import { DateDropdown } from "components/dropdowns"; // helpers @@ -9,49 +8,25 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { TIssue } from "@plane/types"; type Props = { - issueId: string; + issue: TIssue; onChange: (issue: TIssue, data: Partial) => void; - expandedIssues: string[]; disabled: boolean; }; -export const SpreadsheetDueDateColumn: React.FC = ({ issueId, onChange, expandedIssues, disabled }) => { - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - // const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded); - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); +export const SpreadsheetDueDateColumn: React.FC = observer((props: Props) => { + const { issue, onChange, disabled } = props; return ( - <> - {issueDetail && ( -
- onChange(issueDetail, { target_date: data ? renderFormattedPayloadDate(data) : null })} - disabled={disabled} - placeholder="Due date" - buttonVariant="transparent-with-text" - buttonClassName="rounded-none text-left" - buttonContainerClassName="w-full" - /> -
- )} - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId) => ( - - ))} - +
+ onChange(issue, { target_date: data ? renderFormattedPayloadDate(data) : null })} + disabled={disabled} + placeholder="Due date" + buttonVariant="transparent-with-text" + buttonClassName="rounded-none text-left" + buttonContainerClassName="w-full" + /> +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx index 041da65c6..50878ccce 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx @@ -1,56 +1,29 @@ -// hooks -import { useIssueDetail } from "hooks/store"; // components import { EstimateDropdown } from "components/dropdowns"; +import { observer } from "mobx-react-lite"; // types import { TIssue } from "@plane/types"; type Props = { - issueId: string; - onChange: (issue: TIssue, formData: Partial) => void; - expandedIssues: string[]; + issue: TIssue; + onChange: (issue: TIssue, data: Partial) => void; disabled: boolean; }; -export const SpreadsheetEstimateColumn: React.FC = (props) => { - const { issueId, onChange, expandedIssues, disabled } = props; - - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - // const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded); - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); +export const SpreadsheetEstimateColumn: React.FC = observer((props: Props) => { + const { issue, onChange, disabled } = props; return ( - <> - {issueDetail && ( -
- onChange(issueDetail, { estimate_point: data })} - projectId={issueDetail.project_id} - disabled={disabled} - buttonVariant="transparent-with-text" - buttonClassName="rounded-none text-left" - buttonContainerClassName="w-full" - /> -
- )} - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId) => ( - - ))} - +
+ onChange(issue, { estimate_point: data })} + projectId={issue.project_id} + disabled={disabled} + buttonVariant="transparent-with-text" + buttonClassName="rounded-none text-left" + buttonContainerClassName="w-full" + /> +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx new file mode 100644 index 000000000..040000218 --- /dev/null +++ b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx @@ -0,0 +1,123 @@ +//ui +import { CustomMenu } from "@plane/ui"; +import { + ArrowDownWideNarrow, + ArrowUpNarrowWide, + CheckIcon, + ChevronDownIcon, + Eraser, + ListFilter, + MoveRight, +} from "lucide-react"; +//hooks +import useLocalStorage from "hooks/use-local-storage"; +//types +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "@plane/types"; +//constants +import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; + +interface Props { + property: keyof IIssueDisplayProperties; + displayFilters: IIssueDisplayFilterOptions; + handleDisplayFilterUpdate: (data: Partial) => void; +} + +export const SpreadsheetHeaderColumn = (props: Props) => { + const { displayFilters, handleDisplayFilterUpdate, property } = props; + + const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage( + "spreadsheetViewSorting", + "" + ); + const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = useLocalStorage( + "spreadsheetViewActiveSortingProperty", + "" + ); + const propertyDetails = SPREADSHEET_PROPERTY_DETAILS[property]; + + const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => { + handleDisplayFilterUpdate({ order_by: order }); + + setSelectedMenuItem(`${order}_${itemKey}`); + setActiveSortingProperty(order === "-created_at" ? "" : itemKey); + }; + + return ( + +
+ {} + {propertyDetails.title} +
+
+ {activeSortingProperty === property && ( +
+ +
+ )} +
+
+ } + width="xl" + placement="bottom-end" + > + handleOrderBy(propertyDetails.ascendingOrderKey, property)}> +
+
+ + {propertyDetails.ascendingOrderTitle} + + {propertyDetails.descendingOrderTitle} +
+ + {selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` && } +
+
+ handleOrderBy(propertyDetails.descendingOrderKey, property)}> +
+
+ + {propertyDetails.descendingOrderTitle} + + {propertyDetails.ascendingOrderTitle} +
+ + {selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` && ( + + )} +
+
+ {selectedMenuItem && + selectedMenuItem !== "" && + displayFilters?.order_by !== "-created_at" && + selectedMenuItem.includes(property) && ( + handleOrderBy("-created_at", property)} + > +
+ + Clear sorting +
+
+ )} + + ); +}; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/index.ts b/web/components/issues/issue-layouts/spreadsheet/columns/index.ts index a6c4979b3..acfd02fc5 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/index.ts +++ b/web/components/issues/issue-layouts/spreadsheet/columns/index.ts @@ -1,7 +1,5 @@ -export * from "./issue"; export * from "./assignee-column"; export * from "./attachment-column"; -export * from "./columns-list"; export * from "./created-on-column"; export * from "./due-date-column"; export * from "./estimate-column"; @@ -11,4 +9,4 @@ export * from "./priority-column"; export * from "./start-date-column"; export * from "./state-column"; export * from "./sub-issue-column"; -export * from "./updated-on-column"; +export * from "./updated-on-column"; \ No newline at end of file diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/issue/index.ts b/web/components/issues/issue-layouts/spreadsheet/columns/issue/index.ts deleted file mode 100644 index b8d09d1df..000000000 --- a/web/components/issues/issue-layouts/spreadsheet/columns/issue/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./spreadsheet-issue-column"; -export * from "./issue-column"; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx deleted file mode 100644 index 612bba9df..000000000 --- a/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React, { useRef, useState } from "react"; -import { useRouter } from "next/router"; -import { ChevronRight, MoreHorizontal } from "lucide-react"; -// components -import { Tooltip } from "@plane/ui"; -// hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// types -import { TIssue, IIssueDisplayProperties } from "@plane/types"; -import { useProject } from "hooks/store"; - -type Props = { - issue: TIssue; - expanded: boolean; - handleToggleExpand: (issueId: string) => void; - properties: IIssueDisplayProperties; - quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; - canEditProperties: (projectId: string | undefined) => boolean; - nestingLevel: number; -}; - -export const IssueColumn: React.FC = ({ - issue, - expanded, - handleToggleExpand, - properties, - quickActions, - canEditProperties, - nestingLevel, -}) => { - // router - const router = useRouter(); - // hooks - const { getProjectById } = useProject(); - // states - const [isMenuActive, setIsMenuActive] = useState(false); - - const menuActionRef = useRef(null); - - const handleIssuePeekOverview = (issue: TIssue) => { - const { query } = router; - - router.push({ - pathname: router.pathname, - query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id }, - }); - }; - - const paddingLeft = `${nestingLevel * 54}px`; - - useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); - - const customActionButton = ( -
setIsMenuActive(!isMenuActive)} - > - -
- ); - - return ( - <> -
- {properties.key && ( -
-
- - {getProjectById(issue.project_id)?.identifier}-{issue.sequence_id} - - - {canEditProperties(issue.project_id) && ( - - )} -
- - {issue.sub_issues_count > 0 && ( -
- -
- )} -
- )} -
- -
handleIssuePeekOverview(issue)} - > - {issue.name} -
-
-
-
- - ); -}; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx deleted file mode 100644 index d906e522a..000000000 --- a/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from "react"; - -// components -import { IssueColumn } from "components/issues"; -// hooks -import { useIssueDetail } from "hooks/store"; -// types -import { TIssue, IIssueDisplayProperties } from "@plane/types"; - -type Props = { - issueId: string; - expandedIssues: string[]; - setExpandedIssues: React.Dispatch>; - properties: IIssueDisplayProperties; - quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; - canEditProperties: (projectId: string | undefined) => boolean; - nestingLevel?: number; -}; - -export const SpreadsheetIssuesColumn: React.FC = ({ - issueId, - expandedIssues, - setExpandedIssues, - properties, - quickActions, - canEditProperties, - nestingLevel = 0, -}) => { - const handleToggleExpand = (issueId: string) => { - setExpandedIssues((prevState) => { - const newArray = [...prevState]; - const index = newArray.indexOf(issueId); - - if (index > -1) newArray.splice(index, 1); - else newArray.push(issueId); - - return newArray; - }); - }; - - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - // const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded); - - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); - - return ( - <> - {issueDetail && ( - - )} - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( - - ))} - - ); -}; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx index a2fef5a5e..82015056e 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx @@ -1,70 +1,39 @@ import React from "react"; - +import { observer } from "mobx-react-lite"; // components import { IssuePropertyLabels } from "../../properties"; // hooks -import { useIssueDetail, useLabel } from "hooks/store"; +import { useLabel } from "hooks/store"; // types -import { TIssue, IIssueLabel } from "@plane/types"; +import { TIssue } from "@plane/types"; type Props = { - issueId: string; - onChange: (issue: TIssue, formData: Partial) => void; - labels: IIssueLabel[] | undefined; - expandedIssues: string[]; + issue: TIssue; + onChange: (issue: TIssue, data: Partial) => void; disabled: boolean; }; -export const SpreadsheetLabelColumn: React.FC = (props) => { - const { issueId, onChange, labels, expandedIssues, disabled } = props; +export const SpreadsheetLabelColumn: React.FC = observer((props: Props) => { + const { issue, onChange, disabled } = props; // hooks const { labelMap } = useLabel(); - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - // const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded); - - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); - - const defaultLabelOptions = issueDetail?.label_ids?.map((id) => labelMap[id]) || []; + const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || []; return ( - <> - {issueDetail && ( - { - onChange(issueDetail, { label_ids: data }); - }} - className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80" - buttonClassName="px-2.5 h-full" - hideDropdownArrow - maxRender={1} - disabled={disabled} - placeholderText="Select labels" - /> - )} - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( -
- -
- ))} - + { + onChange(issue, { label_ids: data }); + }} + className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80" + buttonClassName="px-2.5 h-full" + hideDropdownArrow + maxRender={1} + disabled={disabled} + placeholderText="Select labels" + /> ); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/link-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/link-column.tsx index a86dcedd7..2d3e7b670 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/link-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/link-column.tsx @@ -1,39 +1,18 @@ import React from "react"; -// hooks -import { useIssueDetail } from "hooks/store"; +import { observer } from "mobx-react-lite"; // types +import { TIssue } from "@plane/types"; type Props = { - issueId: string; - expandedIssues: string[]; + issue: TIssue; }; -export const SpreadsheetLinkColumn: React.FC = (props) => { - const { issueId, expandedIssues } = props; - - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - // const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded); - - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); +export const SpreadsheetLinkColumn: React.FC = observer((props: Props) => { + const { issue } = props; return ( - <> -
- {issueDetail?.link_count} {issueDetail?.link_count === 1 ? "link" : "links"} -
- - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( -
- -
- ))} - +
+ {issue?.link_count} {issue?.link_count === 1 ? "link" : "links"} +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx index 5462a9e13..0a8321740 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx @@ -1,54 +1,29 @@ import React from "react"; -// hooks -import { useIssueDetail } from "hooks/store"; +import { observer } from "mobx-react-lite"; // components import { PriorityDropdown } from "components/dropdowns"; // types import { TIssue } from "@plane/types"; type Props = { - issueId: string; + issue: TIssue; onChange: (issue: TIssue, data: Partial) => void; - expandedIssues: string[]; disabled: boolean; }; -export const SpreadsheetPriorityColumn: React.FC = (props) => { - const { issueId, onChange, expandedIssues, disabled } = props; - // store hooks - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - // derived values - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); - const isExpanded = expandedIssues.indexOf(issueId) > -1; +export const SpreadsheetPriorityColumn: React.FC = observer((props: Props) => { + const { issue, onChange, disabled } = props; return ( - <> - {issueDetail && ( -
- onChange(issueDetail, { priority: data })} - disabled={disabled} - buttonVariant="transparent-with-text" - buttonClassName="rounded-none text-left" - buttonContainerClassName="w-full" - /> -
- )} - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( - - ))} - +
+ onChange(issue, { priority: data })} + disabled={disabled} + buttonVariant="transparent-with-text" + buttonClassName="rounded-none text-left" + buttonContainerClassName="w-full" + /> +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx index 09248e320..778f9cdac 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx @@ -1,6 +1,5 @@ import React from "react"; -// hooks -import { useIssueDetail } from "hooks/store"; +import { observer } from "mobx-react-lite"; // components import { DateDropdown } from "components/dropdowns"; // helpers @@ -9,50 +8,25 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { TIssue } from "@plane/types"; type Props = { - issueId: string; - onChange: (issue: TIssue, formData: Partial) => void; - expandedIssues: string[]; + issue: TIssue; + onChange: (issue: TIssue, data: Partial) => void; disabled: boolean; }; -export const SpreadsheetStartDateColumn: React.FC = ({ issueId, onChange, expandedIssues, disabled }) => { - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - // const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded); - - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); +export const SpreadsheetStartDateColumn: React.FC = observer((props: Props) => { + const { issue, onChange, disabled } = props; return ( - <> - {issueDetail && ( -
- onChange(issueDetail, { start_date: data ? renderFormattedPayloadDate(data) : null })} - disabled={disabled} - placeholder="Start date" - buttonVariant="transparent-with-text" - buttonClassName="rounded-none text-left" - buttonContainerClassName="w-full" - /> -
- )} - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId) => ( - - ))} - +
+ onChange(issue, { start_date: data ? renderFormattedPayloadDate(data) : null })} + disabled={disabled} + placeholder="Start date" + buttonVariant="transparent-with-text" + buttonClassName="rounded-none text-left" + buttonContainerClassName="w-full" + /> +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx index 39508ca37..0050c8acf 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx @@ -1,59 +1,30 @@ import React from "react"; -// hooks -import { useIssueDetail } from "hooks/store"; +import { observer } from "mobx-react-lite"; // components import { StateDropdown } from "components/dropdowns"; // types -import { TIssue, IState } from "@plane/types"; +import { TIssue } from "@plane/types"; type Props = { - issueId: string; + issue: TIssue; onChange: (issue: TIssue, data: Partial) => void; - states: IState[] | undefined; - expandedIssues: string[]; disabled: boolean; }; -export const SpreadsheetStateColumn: React.FC = (props) => { - const { issueId, onChange, states, expandedIssues, disabled } = props; - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); - - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - // const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded); +export const SpreadsheetStateColumn: React.FC = observer((props) => { + const { issue, onChange, disabled } = props; return ( - <> - {issueDetail && ( -
- onChange(issueDetail, { state_id: data })} - disabled={disabled} - buttonVariant="transparent-with-text" - buttonClassName="rounded-none text-left" - buttonContainerClassName="w-full" - /> -
- )} - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId) => ( - - ))} - +
+ onChange(issue, { state_id: data })} + disabled={disabled} + buttonVariant="transparent-with-text" + buttonClassName="rounded-none text-left" + buttonContainerClassName="w-full" + /> +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx index e641c1e01..c0e41d2c0 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx @@ -1,37 +1,18 @@ import React from "react"; +import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail } from "hooks/store"; +import { TIssue } from "@plane/types"; type Props = { - issueId: string; - expandedIssues: string[]; + issue: TIssue; }; -export const SpreadsheetSubIssueColumn: React.FC = (props) => { - const { issueId, expandedIssues } = props; - - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - // const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded); - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); +export const SpreadsheetSubIssueColumn: React.FC = observer((props: Props) => { + const { issue } = props; return ( - <> -
- {issueDetail?.sub_issues_count} {issueDetail?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} -
- - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( -
- -
- ))} - +
+ {issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx index 3ce036d69..f84989192 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx @@ -1,43 +1,19 @@ import React from "react"; -// hooks -// import useSubIssue from "hooks/use-sub-issue"; +import { observer } from "mobx-react-lite"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { useIssueDetail } from "hooks/store"; +import { TIssue } from "@plane/types"; type Props = { - issueId: string; - expandedIssues: string[]; + issue: TIssue; }; -export const SpreadsheetUpdatedOnColumn: React.FC = (props) => { - const { issueId, expandedIssues } = props; - - const isExpanded = expandedIssues.indexOf(issueId) > -1; - - // const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded); - const { subIssues: subIssuesStore, issue } = useIssueDetail(); - - const issueDetail = issue.getIssueById(issueId); - const subIssues = subIssuesStore.subIssuesByIssueId(issueId); - +export const SpreadsheetUpdatedOnColumn: React.FC = observer((props: Props) => { + const { issue } = props; return ( - <> - {issueDetail && ( -
- {renderFormattedDate(issueDetail.updated_at)} -
- )} - - {isExpanded && - subIssues && - subIssues.length > 0 && - subIssues.map((subIssueId: string) => ( -
- -
- ))} - +
+ {renderFormattedDate(issue.updated_at)} +
); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/index.ts b/web/components/issues/issue-layouts/spreadsheet/index.ts index 10fc26219..8f7c4a7fd 100644 --- a/web/components/issues/issue-layouts/spreadsheet/index.ts +++ b/web/components/issues/issue-layouts/spreadsheet/index.ts @@ -1,5 +1,4 @@ export * from "./columns"; export * from "./roots"; -export * from "./spreadsheet-column"; export * from "./spreadsheet-view"; export * from "./quick-add-issue-form"; diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx new file mode 100644 index 000000000..602c1a842 --- /dev/null +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -0,0 +1,193 @@ +import { useRef, useState } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// icons +import { ChevronRight, MoreHorizontal } from "lucide-react"; +// constants +import { SPREADSHEET_PROPERTY_DETAILS, SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; +// components +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +// ui +import { ControlLink, Tooltip } from "@plane/ui"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { useIssueDetail, useProject } from "hooks/store"; +// types +import { IIssueDisplayProperties, TIssue } from "@plane/types"; +import { EIssueActions } from "../types"; + +interface Props { + displayProperties: IIssueDisplayProperties; + isEstimateEnabled: boolean; + quickActions: ( + issue: TIssue, + customActionButton?: React.ReactElement, + portalElement?: HTMLDivElement | null + ) => React.ReactNode; + canEditProperties: (projectId: string | undefined) => boolean; + handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + portalElement: React.MutableRefObject; + nestingLevel: number; + issueId: string; +} + +export const SpreadsheetIssueRow = observer((props: Props) => { + const { + displayProperties, + issueId, + isEstimateEnabled, + nestingLevel, + portalElement, + handleIssues, + quickActions, + canEditProperties, + } = props; + + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + //hooks + const { getProjectById } = useProject(); + const { setPeekIssue } = useIssueDetail(); + // states + const [isMenuActive, setIsMenuActive] = useState(false); + const [isExpanded, setExpanded] = useState(false); + + const menuActionRef = useRef(null); + + const handleIssuePeekOverview = (issue: TIssue) => { + if (workspaceSlug && issue && issue.project_id && issue.id) + setPeekIssue({ workspaceSlug: workspaceSlug.toString(), projectId: issue.project_id, issueId: issue.id }); + }; + + const { subIssues: subIssuesStore, issue } = useIssueDetail(); + + const issueDetail = issue.getIssueById(issueId); + const subIssues = subIssuesStore.subIssuesByIssueId(issueId); + + const paddingLeft = `${nestingLevel * 54}px`; + + useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); + + const handleToggleExpand = () => { + setExpanded((prevState) => { + if (!prevState && workspaceSlug && issueDetail) + subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issueDetail.project_id, issueDetail.id); + return !prevState; + }); + }; + + const customActionButton = ( +
setIsMenuActive(!isMenuActive)} + > + +
+ ); + + if (!issueDetail) return null; + + const disableUserActions = !canEditProperties(issueDetail.project_id); + + return ( + <> + + {/* first column/ issue name and key column */} + + +
+
+ + {getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id} + + + {canEditProperties(issueDetail.project_id) && ( + + )} +
+ + {issueDetail.sub_issues_count > 0 && ( +
+ +
+ )} +
+
+ handleIssuePeekOverview(issueDetail)} + className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + > +
+ +
+ {issueDetail.name} +
+
+
+
+ + {/* Rest of the columns */} + {SPREADSHEET_PROPERTY_LIST.map((property) => { + const { Column } = SPREADSHEET_PROPERTY_DETAILS[property]; + + const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true; + + return ( + + + ) => + handleIssues({ ...issue, ...data }, EIssueActions.UPDATE) + } + disabled={disableUserActions} + /> + + + ); + })} + + + {isExpanded && + subIssues && + subIssues.length > 0 && + subIssues.map((subIssueId: string) => ( + + ))} + + ); +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx index 603276b3b..605e8bea1 100644 --- a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx @@ -148,7 +148,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => reset({ ...defaultValues }); - const payload = createIssuePayload(currentWorkspace, currentProjectDetails, { + const payload = createIssuePayload(currentProjectDetails.id, { ...(prePopulatedData ?? {}), ...formData, }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx index 7420d1e48..28b766cd1 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx @@ -1,43 +1,40 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store import { useIssues } from "hooks/store"; // components import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; +// types import { EIssueActions } from "../../types"; import { TIssue } from "@plane/types"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; +// constants import { EIssuesStoreType } from "constants/issue"; -export const ProjectViewSpreadsheetLayout: React.FC = observer(() => { +export interface IViewSpreadsheetLayout { + issueActions: { + [EIssueActions.DELETE]: (issue: TIssue) => Promise; + [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; + [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + }; +} + +export const ProjectViewSpreadsheetLayout: React.FC = observer((props) => { + const { issueActions } = props; + // router const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string }; + const { viewId } = router.query; const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx deleted file mode 100644 index 0a0fbe9c0..000000000 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { - ArrowDownWideNarrow, - ArrowUpNarrowWide, - CheckIcon, - ChevronDownIcon, - Eraser, - ListFilter, - MoveRight, -} from "lucide-react"; -// hooks -import useLocalStorage from "hooks/use-local-storage"; -// components -import { - SpreadsheetAssigneeColumn, - SpreadsheetAttachmentColumn, - SpreadsheetCreatedOnColumn, - SpreadsheetDueDateColumn, - SpreadsheetEstimateColumn, - SpreadsheetLabelColumn, - SpreadsheetLinkColumn, - SpreadsheetPriorityColumn, - SpreadsheetStartDateColumn, - SpreadsheetStateColumn, - SpreadsheetSubIssueColumn, - SpreadsheetUpdatedOnColumn, -} from "components/issues"; -// ui -import { CustomMenu } from "@plane/ui"; -// types -import { TIssue, IIssueDisplayFilterOptions, IIssueLabel, IState, TIssueOrderByOptions } from "@plane/types"; -// constants -import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; - -type Props = { - canEditProperties: (projectId: string | undefined) => boolean; - displayFilters: IIssueDisplayFilterOptions; - expandedIssues: string[]; - handleDisplayFilterUpdate: (data: Partial) => void; - handleUpdateIssue: (issue: TIssue, data: Partial) => void; - issues: TIssue[] | undefined; - property: string; - labels?: IIssueLabel[] | undefined; - states?: IState[] | undefined; -}; - -export const SpreadsheetColumn: React.FC = (props) => { - const { - canEditProperties, - displayFilters, - expandedIssues, - handleDisplayFilterUpdate, - handleUpdateIssue, - issues, - property, - labels, - states, - } = props; - - const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage( - "spreadsheetViewSorting", - "" - ); - const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = useLocalStorage( - "spreadsheetViewActiveSortingProperty", - "" - ); - - const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => { - handleDisplayFilterUpdate({ order_by: order }); - - setSelectedMenuItem(`${order}_${itemKey}`); - setActiveSortingProperty(order === "-created_at" ? "" : itemKey); - }; - - const propertyDetails = SPREADSHEET_PROPERTY_DETAILS[property]; - - return ( -
-
- -
- {} - {propertyDetails.title} -
-
- {activeSortingProperty === property && ( -
- -
- )} -
-
- } - width="xl" - placement="bottom-end" - > - handleOrderBy(propertyDetails.ascendingOrderKey, property)}> -
-
- - {propertyDetails.ascendingOrderTitle} - - {propertyDetails.descendingOrderTitle} -
- - {selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` && ( - - )} -
-
- handleOrderBy(propertyDetails.descendingOrderKey, property)}> -
-
- - {propertyDetails.descendingOrderTitle} - - {propertyDetails.ascendingOrderTitle} -
- - {selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` && ( - - )} -
-
- {selectedMenuItem && - selectedMenuItem !== "" && - displayFilters?.order_by !== "-created_at" && - selectedMenuItem.includes(property) && ( - handleOrderBy("-created_at", property)} - > -
- - Clear sorting -
-
- )} - -
- -
- {issues?.map((issue) => { - const disableUserActions = !canEditProperties(issue.project_id); - return ( -
- {property === "state" ? ( - ) => handleUpdateIssue(issue, data)} - states={states} - /> - ) : property === "priority" ? ( - ) => handleUpdateIssue(issue, data)} - /> - ) : property === "estimate" ? ( - ) => handleUpdateIssue(issue, data)} - /> - ) : property === "assignee" ? ( - ) => handleUpdateIssue(issue, data)} - /> - ) : property === "labels" ? ( - ) => handleUpdateIssue(issue, data)} - /> - ) : property === "start_date" ? ( - ) => handleUpdateIssue(issue, data)} - /> - ) : property === "due_date" ? ( - ) => handleUpdateIssue(issue, data)} - /> - ) : property === "created_on" ? ( - - ) : property === "updated_on" ? ( - - ) : property === "link" ? ( - - ) : property === "attachment_count" ? ( - - ) : property === "sub_issue_count" ? ( - - ) : null} -
- ); - })} -
-
- ); -}; diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx new file mode 100644 index 000000000..704c9f904 --- /dev/null +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx @@ -0,0 +1,59 @@ +// ui +import { LayersIcon } from "@plane/ui"; +// types +import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; +// constants +import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; +// components +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +import { SpreadsheetHeaderColumn } from "./columns/header-column"; + + +interface Props { + displayProperties: IIssueDisplayProperties; + displayFilters: IIssueDisplayFilterOptions; + handleDisplayFilterUpdate: (data: Partial) => void; + isEstimateEnabled: boolean; +} + +export const SpreadsheetHeader = (props: Props) => { + const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled } = props; + + return ( + + + + + + #ID + + + + + Issue + + + + {SPREADSHEET_PROPERTY_LIST.map((property) => { + const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true; + + return ( + + + + + + ); + })} + + + ); +}; diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx new file mode 100644 index 000000000..e287c6d84 --- /dev/null +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx @@ -0,0 +1,63 @@ +import { observer } from "mobx-react-lite"; +//types +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types"; +import { EIssueActions } from "../types"; +//components +import { SpreadsheetIssueRow } from "./issue-row"; +import { SpreadsheetHeader } from "./spreadsheet-header"; + +type Props = { + displayProperties: IIssueDisplayProperties; + displayFilters: IIssueDisplayFilterOptions; + handleDisplayFilterUpdate: (data: Partial) => void; + issueIds: string[]; + isEstimateEnabled: boolean; + quickActions: ( + issue: TIssue, + customActionButton?: React.ReactElement, + portalElement?: HTMLDivElement | null + ) => React.ReactNode; + handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + canEditProperties: (projectId: string | undefined) => boolean; + portalElement: React.MutableRefObject; +}; + +export const SpreadsheetTable = observer((props: Props) => { + const { + displayProperties, + displayFilters, + handleDisplayFilterUpdate, + issueIds, + isEstimateEnabled, + portalElement, + quickActions, + handleIssues, + canEditProperties, + } = props; + + return ( + + + + {issueIds.map((id) => ( + + ))} + +
+ ); +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index b86eabf54..e99b17850 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -1,26 +1,25 @@ -import React, { useEffect, useRef, useState } from "react"; -import { useRouter } from "next/router"; +import React, { useEffect, useRef } from "react"; import { observer } from "mobx-react-lite"; // components -import { - IssuePeekOverview, - SpreadsheetColumnsList, - SpreadsheetIssuesColumn, - SpreadsheetQuickAddIssueForm, -} from "components/issues"; -import { Spinner, LayersIcon } from "@plane/ui"; +import { Spinner } from "@plane/ui"; +import { SpreadsheetQuickAddIssueForm } from "components/issues"; +import { SpreadsheetTable } from "./spreadsheet-table"; // types -import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState } from "@plane/types"; +import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { EIssueActions } from "../types"; +//hooks +import { useProject } from "hooks/store"; type Props = { displayProperties: IIssueDisplayProperties; displayFilters: IIssueDisplayFilterOptions; handleDisplayFilterUpdate: (data: Partial) => void; - issues: TIssue[] | undefined; - labels?: IIssueLabel[] | undefined; - states?: IState[] | undefined; - quickActions: (issue: TIssue, customActionButton: any) => React.ReactNode; + issueIds: string[] | undefined; + quickActions: ( + issue: TIssue, + customActionButton?: React.ReactElement, + portalElement?: HTMLDivElement | null + ) => React.ReactNode; handleIssues: (issue: TIssue, action: EIssueActions) => Promise; openIssuesListModal?: (() => void) | null; quickAddCallback?: ( @@ -40,9 +39,7 @@ export const SpreadsheetView: React.FC = observer((props) => { displayProperties, displayFilters, handleDisplayFilterUpdate, - issues, - labels, - states, + issueIds, quickActions, handleIssues, quickAddCallback, @@ -52,19 +49,36 @@ export const SpreadsheetView: React.FC = observer((props) => { disableIssueCreation, } = props; // states - const [expandedIssues, setExpandedIssues] = useState([]); - const [isScrolled, setIsScrolled] = useState(false); + const isScrolled = useRef(false); // refs - const containerRef = useRef(null); - // router - const router = useRouter(); - const { workspaceSlug, peekIssueId, peekProjectId } = router.query; + const containerRef = useRef(null); + const portalRef = useRef(null); + + const { currentProjectDetails } = useProject(); + + const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null; const handleScroll = () => { if (!containerRef.current) return; - const scrollLeft = containerRef.current.scrollLeft; - setIsScrolled(scrollLeft > 0); + + const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns + const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers + + //The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly + if (scrollLeft > 0 !== isScrolled.current) { + const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child"); + + for (let i = 0; i < firtColumns.length; i++) { + const shadow = i === 0 ? headerShadow : columnShadow; + if (scrollLeft > 0) { + (firtColumns[i] as HTMLElement).style.boxShadow = shadow; + } else { + (firtColumns[i] as HTMLElement).style.boxShadow = "none"; + } + } + isScrolled.current = scrollLeft > 0; + } }; useEffect(() => { @@ -77,7 +91,7 @@ export const SpreadsheetView: React.FC = observer((props) => { }; }, []); - if (!issues || issues.length === 0) + if (!issueIds || issueIds.length === 0) return (
@@ -85,115 +99,28 @@ export const SpreadsheetView: React.FC = observer((props) => { ); return ( -
-
-
- {issues && issues.length > 0 && ( - <> -
-
-
- {displayProperties.key && ( - - #ID - - )} - - - Issue - -
- - {issues.map((issue, index) => - issue ? ( - - ) : null - )} -
-
- - handleIssues({ ...issue, ...data }, EIssueActions.UPDATE)} - issues={issues} - labels={labels} - states={states} - /> - +
+
+
+ +
+
+
+ {enableQuickCreateIssue && !disableIssueCreation && ( + )} -
{/* empty div to show right most border */} -
- -
-
- {enableQuickCreateIssue && !disableIssueCreation && ( - - )} -
- - {/* {!disableUserActions && - !isInlineCreateIssueFormOpen && - (type === "issue" ? ( - - ) : ( - - - New Issue - - } - optionsClassName="left-5 !w-36" - noBorder - > - setIsInlineCreateIssueFormOpen(true)}> - Create new - - {openIssuesListModal && ( - Add an existing issue - )} - - ))} */}
- {workspaceSlug && peekIssueId && peekProjectId && ( - await handleIssues(issueToUpdate, EIssueActions.UPDATE)} - /> - )}
); }); diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 866e26e75..91ce10e05 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -1,11 +1,12 @@ import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui"; -import { ISSUE_PRIORITIES, ISSUE_STATE_GROUPS } from "constants/issue"; +import { ISSUE_PRIORITIES } from "constants/issue"; import { renderEmoji } from "helpers/emoji.helper"; import { ILabelRootStore } from "store/label"; import { IMemberRootStore } from "store/member"; import { IProjectStore } from "store/project/project.store"; import { IStateStore } from "store/state.store"; import { GroupByColumnTypes, IGroupByColumn } from "@plane/types"; +import { STATE_GROUPS } from "constants/state"; export const getGroupByColumns = ( groupBy: GroupByColumnTypes | null, @@ -31,7 +32,7 @@ export const getGroupByColumns = ( case "created_by": return getCreatedByColumns(member) as any; default: - if (includeNone) return [{ id: `null`, name: `All Issues`, payload: {}, Icon: undefined }]; + if (includeNone) return [{ id: `null`, name: `All Issues`, payload: {}, icon: undefined }]; } }; @@ -48,8 +49,8 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined return { id: project.id, name: project.name, - Icon:
{renderEmoji(project.emoji || "")}
, - payload: { project: project.id }, + icon:
{renderEmoji(project.emoji || "")}
, + payload: { project_id: project.id }, }; }) as any; }; @@ -61,22 +62,22 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine return projectStates.map((state) => ({ id: state.id, name: state.name, - Icon: ( + icon: (
), - payload: { state: state.id }, + payload: { state_id: state.id }, })) as any; }; const getStateGroupColumns = () => { - const stateGroups = ISSUE_STATE_GROUPS; + const stateGroups = STATE_GROUPS; - return stateGroups.map((stateGroup) => ({ + return Object.values(stateGroups).map((stateGroup) => ({ id: stateGroup.key, - name: stateGroup.title, - Icon: ( + name: stateGroup.label, + icon: (
@@ -91,7 +92,7 @@ const getPriorityColumns = () => { return priorities.map((priority) => ({ id: priority.key, name: priority.title, - Icon: , + icon: , payload: { priority: priority.key }, })); }; @@ -108,10 +109,10 @@ const getLabelsColumns = (projectLabel: ILabelRootStore) => { return labels.map((label) => ({ id: label.id, name: label.name, - Icon: ( + icon: (
), - payload: { labels: [label.id] }, + payload: label?.id === "None" ? {} : { label_ids: [label.id] }, })); }; @@ -123,17 +124,17 @@ const getAssigneeColumns = (member: IMemberRootStore) => { if (!projectMemberIds) return; - const assigneeColumns = projectMemberIds.map((memberId) => { + const assigneeColumns: any = projectMemberIds.map((memberId) => { const member = getUserDetails(memberId); return { id: memberId, name: member?.display_name || "", - Icon: , - payload: { assignees: [memberId] }, + icon: , + payload: { assignee_ids: [memberId] }, }; }); - assigneeColumns.push({ id: "None", name: "None", Icon: , payload: { assignees: [""] } }); + assigneeColumns.push({ id: "None", name: "None", icon: , payload: {} }); return assigneeColumns; }; @@ -151,8 +152,8 @@ const getCreatedByColumns = (member: IMemberRootStore) => { return { id: memberId, name: member?.display_name || "", - Icon: , - payload: { assignees: [memberId] }, + icon: , + payload: {}, }; }); }; diff --git a/web/components/issues/issue-links/index.ts b/web/components/issues/issue-links/index.ts deleted file mode 100644 index 1efe34c51..000000000 --- a/web/components/issues/issue-links/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./root"; diff --git a/web/components/issues/issue-modal/draft-issue-layout.tsx b/web/components/issues/issue-modal/draft-issue-layout.tsx index 15cba3c04..274df2981 100644 --- a/web/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/components/issues/issue-modal/draft-issue-layout.tsx @@ -14,6 +14,8 @@ import type { TIssue } from "@plane/types"; export interface DraftIssueProps { changesMade: Partial | null; data?: Partial; + isCreateMoreToggleEnabled: boolean; + onCreateMoreToggleChange: (value: boolean) => void; onChange: (formData: Partial | null) => void; onClose: (saveDraftIssueInLocalStorage?: boolean) => void; onSubmit: (formData: Partial) => Promise; @@ -23,7 +25,16 @@ export interface DraftIssueProps { const issueDraftService = new IssueDraftService(); export const DraftIssueLayout: React.FC = observer((props) => { - const { changesMade, data, onChange, onClose, onSubmit, projectId } = props; + const { + changesMade, + data, + onChange, + onClose, + onSubmit, + projectId, + isCreateMoreToggleEnabled, + onCreateMoreToggleChange, + } = props; // states const [issueDiscardModal, setIssueDiscardModal] = useState(false); // router @@ -76,7 +87,15 @@ export const DraftIssueLayout: React.FC = observer((props) => { onClose(false); }} /> - + ); }); diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 7f00f6216..8fe54c21c 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -1,4 +1,4 @@ -import React, { FC, useState, useRef } from "react"; +import React, { FC, useState, useRef, useEffect } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; @@ -6,7 +6,7 @@ import { LayoutPanelTop, Sparkle, X } from "lucide-react"; // editor import { RichTextEditorWithRef } from "@plane/rich-text-editor"; // hooks -import { useApplication, useEstimate, useMention, useProject } from "hooks/store"; +import { useApplication, useEstimate, useIssueDetail, useMention, useProject } from "hooks/store"; import useToast from "hooks/use-toast"; // services import { AIService } from "services/ai.service"; @@ -51,6 +51,8 @@ const defaultValues: Partial = { export interface IssueFormProps { data?: Partial; + isCreateMoreToggleEnabled: boolean; + onCreateMoreToggleChange: (value: boolean) => void; onChange?: (formData: Partial | null) => void; onClose: () => void; onSubmit: (values: Partial) => Promise; @@ -62,14 +64,14 @@ const aiService = new AIService(); const fileService = new FileService(); export const IssueFormRoot: FC = observer((props) => { - const { data, onChange, onClose, onSubmit, projectId } = props; + const { data, onChange, onClose, onSubmit, projectId, isCreateMoreToggleEnabled, onCreateMoreToggleChange } = props; // states const [labelModal, setLabelModal] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); const [selectedParentIssue, setSelectedParentIssue] = useState(null); const [gptAssistantModal, setGptAssistantModal] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); - const [createMore, setCreateMore] = useState(false); + // refs const editorRef = useRef(null); // router @@ -82,6 +84,9 @@ export const IssueFormRoot: FC = observer((props) => { const { getProjectById } = useProject(); const { areEstimatesEnabledForProject } = useEstimate(); const { mentionHighlights, mentionSuggestions } = useMention(); + const { + issue: { getIssueById }, + } = useIssueDetail(); // toast alert const { setToastAlert } = useToast(); // form info @@ -176,6 +181,28 @@ export const IssueFormRoot: FC = observer((props) => { const projectDetails = getProjectById(projectId); + // executing this useEffect when the parent_id coming from the component prop + useEffect(() => { + const parentId = watch("parent_id") || undefined; + if (!parentId) return; + if (parentId === selectedParentIssue?.id || selectedParentIssue) return; + + const issue = getIssueById(parentId); + if (!issue) return; + + const projectDetails = getProjectById(issue.project_id); + if (!projectDetails) return; + + setSelectedParentIssue({ + id: issue.id, + name: issue.name, + project_id: issue.project_id, + project__identifier: projectDetails.identifier, + project__name: projectDetails.name, + sequence_id: issue.sequence_id, + } as ISearchIssueResponse); + }, [watch, getIssueById, getProjectById, selectedParentIssue]); + return ( <> {projectId && ( @@ -209,6 +236,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" + tabIndex={19} />
)} @@ -238,6 +266,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); setSelectedParentIssue(null); }} + tabIndex={20} />
@@ -268,12 +297,13 @@ export const IssueFormRoot: FC = observer((props) => { hasError={Boolean(errors.name)} placeholder="Issue Title" className="resize-none text-xl w-full" + tabIndex={1} /> )} />
- {issueName && issueName.trim() !== "" && ( + {issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
)} @@ -374,6 +408,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" + tabIndex={7} />
)} @@ -394,6 +429,7 @@ export const IssueFormRoot: FC = observer((props) => { buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""} placeholder="Assignees" multiple + tabIndex={8} />
)} @@ -411,6 +447,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} projectId={projectId} + tabIndex={9} />
)} @@ -429,6 +466,7 @@ export const IssueFormRoot: FC = observer((props) => { buttonVariant="border-with-text" placeholder="Start date" maxDate={maxDate ?? undefined} + tabIndex={10} />
)} @@ -447,6 +485,7 @@ export const IssueFormRoot: FC = observer((props) => { buttonVariant="border-with-text" placeholder="Due date" minDate={minDate ?? undefined} + tabIndex={11} />
)} @@ -465,6 +504,7 @@ export const IssueFormRoot: FC = observer((props) => { }} value={value} buttonVariant="border-with-text" + tabIndex={12} />
)} @@ -484,6 +524,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" + tabIndex={13} />
)} @@ -503,6 +544,7 @@ export const IssueFormRoot: FC = observer((props) => { }} projectId={projectId} buttonVariant="border-with-text" + tabIndex={14} />
)} @@ -532,6 +574,7 @@ export const IssueFormRoot: FC = observer((props) => { } placement="bottom-start" + tabIndex={15} > {watch("parent_id") ? ( <> @@ -577,18 +620,22 @@ export const IssueFormRoot: FC = observer((props) => {
setCreateMore((prevData) => !prevData)} + onClick={() => onCreateMoreToggleChange(!isCreateMoreToggleEnabled)} + onKeyDown={(e) => { + if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled); + }} + tabIndex={16} >
- {}} size="sm" /> + {}} size="sm" />
Create more
- -
diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 975d4f09e..99e60eab2 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -1,9 +1,8 @@ -import React, { useState } from "react"; -import { useRouter } from "next/router"; +import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; // hooks -import { useIssues, useProject } from "hooks/store"; +import { useApplication, useCycle, useIssues, useModule, useProject, useUser, useWorkspace } from "hooks/store"; import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; // components @@ -12,127 +11,246 @@ import { IssueFormRoot } from "./form"; // types import type { TIssue } from "@plane/types"; // constants -import { EIssuesStoreType } from "constants/issue"; +import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue"; export interface IssuesModalProps { data?: Partial; isOpen: boolean; onClose: () => void; - onSubmit?: (res: Partial) => Promise; + onSubmit?: (res: TIssue) => Promise; withDraftIssueWrapper?: boolean; + storeType?: TCreateModalStoreTypes; } export const CreateUpdateIssueModal: React.FC = observer((props) => { - const { data, isOpen, onClose, onSubmit, withDraftIssueWrapper = true } = props; + const { + data, + isOpen, + onClose, + onSubmit, + withDraftIssueWrapper = true, + storeType = EIssuesStoreType.PROJECT, + } = props; // states const [changesMade, setChangesMade] = useState | null>(null); - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const [createMore, setCreateMore] = useState(false); + const [activeProjectId, setActiveProjectId] = useState(null); // store hooks + const { + eventTracker: { postHogEventTracker }, + } = useApplication(); + const { currentUser } = useUser(); + const { + router: { workspaceSlug, projectId, cycleId, moduleId, viewId: projectViewId }, + } = useApplication(); + const { currentWorkspace } = useWorkspace(); const { workspaceProjectIds } = useProject(); - const { - issues: { createIssue, updateIssue }, - } = useIssues(EIssuesStoreType.PROJECT); - const { - issues: { addIssueToCycle }, - } = useIssues(EIssuesStoreType.CYCLE); - const { - issues: { addIssueToModule }, - } = useIssues(EIssuesStoreType.MODULE); + const { fetchCycleDetails } = useCycle(); + const { fetchModuleDetails } = useModule(); + const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); + const { issues: moduleIssues } = useIssues(EIssuesStoreType.MODULE); + const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE); + const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW); + const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE); + // store mapping based on current store + const issueStores = { + [EIssuesStoreType.PROJECT]: { + store: projectIssues, + dataIdToUpdate: activeProjectId, + viewId: undefined, + }, + [EIssuesStoreType.PROJECT_VIEW]: { + store: viewIssues, + dataIdToUpdate: activeProjectId, + viewId: projectViewId, + }, + [EIssuesStoreType.PROFILE]: { + store: profileIssues, + dataIdToUpdate: currentUser?.id || undefined, + viewId: undefined, + }, + [EIssuesStoreType.CYCLE]: { + store: cycleIssues, + dataIdToUpdate: activeProjectId, + viewId: cycleId, + }, + [EIssuesStoreType.MODULE]: { + store: moduleIssues, + dataIdToUpdate: activeProjectId, + viewId: moduleId, + }, + }; // toast alert const { setToastAlert } = useToast(); // local storage const { setValue: setLocalStorageDraftIssue } = useLocalStorage("draftedIssue", {}); + // current store details + const { store: currentIssueStore, viewId, dataIdToUpdate } = issueStores[storeType]; + + useEffect(() => { + // if modal is closed, reset active project to null + // and return to avoid activeProjectId being set to some other project + if (!isOpen) { + setActiveProjectId(null); + return; + } + + // if data is present, set active project to the project of the + // issue. This has more priority than the project in the url. + if (data && data.project_id) { + setActiveProjectId(data.project_id); + return; + } + + // if data is not present, set active project to the project + // in the url. This has the least priority. + if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId) + setActiveProjectId(projectId ?? workspaceProjectIds?.[0]); + }, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]); + + const addIssueToCycle = async (issue: TIssue, cycleId: string) => { + if (!workspaceSlug || !activeProjectId) return; + + await cycleIssues.addIssueToCycle(workspaceSlug, issue.project_id, cycleId, [issue.id]); + fetchCycleDetails(workspaceSlug, activeProjectId, cycleId); + }; + + const addIssueToModule = async (issue: TIssue, moduleId: string) => { + if (!workspaceSlug || !activeProjectId) return; + + await moduleIssues.addIssueToModule(workspaceSlug, activeProjectId, moduleId, [issue.id]); + fetchModuleDetails(workspaceSlug, activeProjectId, moduleId); + }; + + const handleCreateMoreToggleChange = (value: boolean) => { + setCreateMore(value); + }; const handleClose = (saveDraftIssueInLocalStorage?: boolean) => { if (changesMade && saveDraftIssueInLocalStorage) { const draftIssue = JSON.stringify(changesMade); - setLocalStorageDraftIssue(draftIssue); } - + setActiveProjectId(null); onClose(); }; - const handleCreateIssue = async (payload: Partial): Promise => { - if (!workspaceSlug || !payload.project_id) return null; + const handleCreateIssue = async (payload: Partial): Promise => { + if (!workspaceSlug || !dataIdToUpdate) return; - await createIssue(workspaceSlug.toString(), payload.project_id, payload) - .then(async (res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - handleClose(); - return res; - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be created. Please try again.", - }); + try { + const response = await currentIssueStore.createIssue(workspaceSlug, dataIdToUpdate, payload, viewId); + if (!response) throw new Error(); + + currentIssueStore.fetchIssues(workspaceSlug, dataIdToUpdate, "mutation", viewId); + + if (payload.cycle_id && payload.cycle_id !== "" && storeType !== EIssuesStoreType.CYCLE) + await addIssueToCycle(response, payload.cycle_id); + if (payload.module_id && payload.module_id !== "" && storeType !== EIssuesStoreType.MODULE) + await addIssueToModule(response, payload.module_id); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", }); - - return null; + postHogEventTracker( + "ISSUE_CREATED", + { + ...response, + state: "SUCCESS", + }, + { + isGrouping: true, + groupType: "Workspace_metrics", + groupId: currentWorkspace?.id!, + } + ); + !createMore && handleClose(); + return response; + } catch (error) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }); + postHogEventTracker( + "ISSUE_CREATED", + { + state: "FAILED", + }, + { + isGrouping: true, + groupType: "Workspace_metrics", + groupId: currentWorkspace?.id!, + } + ); + } }; - const handleUpdateIssue = async (payload: Partial): Promise => { - if (!workspaceSlug || !payload.project_id || !data?.id) return null; + const handleUpdateIssue = async (payload: Partial): Promise => { + if (!workspaceSlug || !dataIdToUpdate || !data?.id) return; - await updateIssue(workspaceSlug.toString(), payload.project_id, data.id, payload) - .then((res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue updated successfully.", - }); - handleClose(); - return { ...payload, ...res }; - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be updated. Please try again.", - }); + try { + const response = await currentIssueStore.updateIssue(workspaceSlug, dataIdToUpdate, data.id, payload, viewId); + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue updated successfully.", }); - - return null; + postHogEventTracker( + "ISSUE_UPDATED", + { + ...response, + state: "SUCCESS", + }, + { + isGrouping: true, + groupType: "Workspace_metrics", + groupId: currentWorkspace?.id!, + } + ); + handleClose(); + return response; + } catch (error) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }); + postHogEventTracker( + "ISSUE_UPDATED", + { + state: "FAILED", + }, + { + isGrouping: true, + groupType: "Workspace_metrics", + groupId: currentWorkspace?.id!, + } + ); + } }; const handleFormSubmit = async (formData: Partial) => { - if (!workspaceSlug || !formData.project_id) return; + if (!workspaceSlug || !dataIdToUpdate || !storeType) return; const payload: Partial = { ...formData, description_html: formData.description_html ?? "

", }; - let res: TIssue | null = null; - if (!data?.id) res = await handleCreateIssue(payload); - else res = await handleUpdateIssue(payload); + let response: TIssue | undefined = undefined; + if (!data?.id) response = await handleCreateIssue(payload); + else response = await handleUpdateIssue(payload); - // add issue to cycle if cycle is selected, and cycle is different from current cycle - if (formData.cycle_id && res && (!data?.id || formData.cycle_id !== data?.cycle_id)) - await addIssueToCycle(workspaceSlug.toString(), formData.project_id, formData.cycle_id, [res.id]); - - // add issue to module if module is selected, and module is different from current module - if (formData.module_id && res && (!data?.id || formData.module_id !== data?.module_id)) - await addIssueToModule(workspaceSlug.toString(), formData.project_id, formData.module_id, [res.id]); - - if (res && onSubmit) await onSubmit(res); + if (response != undefined && onSubmit) await onSubmit(response); }; const handleFormChange = (formData: Partial | null) => setChangesMade(formData); // don't open the modal if there are no projects - if (!workspaceProjectIds || workspaceProjectIds.length === 0) return null; - - // if project id is present in the router query, use that as the selected project id, otherwise use the first project id - const selectedProjectId = projectId ? projectId.toString() : workspaceProjectIds[0]; + if (!workspaceProjectIds || workspaceProjectIds.length === 0 || !activeProjectId) return null; return ( @@ -164,18 +282,30 @@ export const CreateUpdateIssueModal: React.FC = observer((prop {withDraftIssueWrapper ? ( ) : ( handleClose(false)} + isCreateMoreToggleEnabled={createMore} + onCreateMoreToggleChange={handleCreateMoreToggleChange} onSubmit={handleFormSubmit} - projectId={selectedProjectId} + projectId={activeProjectId} /> )} diff --git a/web/components/issues/issue-reaction.tsx b/web/components/issues/issue-reaction.tsx deleted file mode 100644 index 37d0599e4..000000000 --- a/web/components/issues/issue-reaction.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { observer } from "mobx-react-lite"; -// hooks -import { useUser } from "hooks/store"; -import useIssueReaction from "hooks/use-issue-reaction"; -// components -import { ReactionSelector } from "components/core"; -// string helpers -import { renderEmoji } from "helpers/emoji.helper"; - -// types -type Props = { - workspaceSlug: string; - projectId: string; - issueId: string; -}; - -export const IssueReaction: React.FC = observer((props) => { - const { workspaceSlug, projectId, issueId } = props; - - const { currentUser } = useUser(); - - const { reactions, groupedReactions, handleReactionCreate, handleReactionDelete } = useIssueReaction( - workspaceSlug, - projectId, - issueId - ); - - const handleReactionClick = (reaction: string) => { - if (!workspaceSlug || !projectId || !issueId) return; - - const isSelected = reactions?.some((r) => r.actor === currentUser?.id && r.reaction === reaction); - - if (isSelected) { - handleReactionDelete(reaction); - } else { - handleReactionCreate(reaction); - } - }; - - return ( -
- reaction.actor === currentUser?.id).map((r) => r.reaction) || []} - onSelect={handleReactionClick} - /> - - {Object.keys(groupedReactions || {}).map( - (reaction) => - groupedReactions?.[reaction]?.length && - groupedReactions[reaction].length > 0 && ( - - ) - )} -
- ); -}); diff --git a/web/components/issues/label.tsx b/web/components/issues/label.tsx index c66ded153..1361ff7d1 100644 --- a/web/components/issues/label.tsx +++ b/web/components/issues/label.tsx @@ -9,7 +9,7 @@ type Props = { export const ViewIssueLabel: React.FC = ({ labelDetails, maxRender = 1 }) => ( <> - {labelDetails.length > 0 ? ( + {labelDetails?.length > 0 ? ( labelDetails.length <= maxRender ? ( <> {labelDetails.map((label) => ( diff --git a/web/components/issues/main-content.tsx b/web/components/issues/main-content.tsx deleted file mode 100644 index 2863cb3b9..000000000 --- a/web/components/issues/main-content.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import Link from "next/link"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import useSWR, { mutate } from "swr"; -import { MinusCircle } from "lucide-react"; -// hooks -import { useApplication, useIssues, useProject, useProjectState, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; -// services -import { IssueService, IssueCommentService } from "services/issue"; -// components -import { - IssueAttachmentRoot, - AddComment, - IssueActivitySection, - IssueDescriptionForm, - IssueReaction, - IssueUpdateStatus, -} from "components/issues"; -import { useState } from "react"; -import { SubIssuesRoot } from "./sub-issues"; -// ui -import { CustomMenu, LayersIcon, StateGroupIcon } from "@plane/ui"; -// types -import { TIssue, IIssueActivity } from "@plane/types"; -// fetch-keys -import { PROJECT_ISSUES_ACTIVITY, SUB_ISSUES } from "constants/fetch-keys"; -// constants -import { EUserProjectRoles } from "constants/project"; - -type Props = { - issueDetails: TIssue; - submitChanges: (formData: Partial) => Promise; - uneditable?: boolean; -}; - -// services -const issueService = new IssueService(); -const issueCommentService = new IssueCommentService(); - -export const IssueMainContent: React.FC = observer((props) => { - const { issueDetails, submitChanges, uneditable = false } = props; - // states - const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); - // router - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - // toast alert - const { setToastAlert } = useToast(); - const { - eventTracker: { postHogEventTracker }, - } = useApplication(); - const { - currentUser, - membership: { currentProjectRole }, - } = useUser(); - const { currentWorkspace } = useWorkspace(); - const { getProjectById } = useProject(); - const { projectStates, getProjectStates } = useProjectState(); - const { issueMap } = useIssues(); - - const projectDetails = projectId ? getProjectById(projectId.toString()) : null; - const currentIssueState = projectStates?.find((s) => s.id === issueDetails.state_id); - - const { data: siblingIssues } = useSWR( - workspaceSlug && projectId && issueDetails?.parent_id ? SUB_ISSUES(issueDetails.parent_id) : null, - workspaceSlug && projectId && issueDetails?.parent_id - ? () => issueService.subIssues(workspaceSlug.toString(), projectId.toString(), issueDetails.parent_id ?? "") - : null - ); - const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id); - - const { data: issueActivity, mutate: mutateIssueActivity } = useSWR( - workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId.toString()) : null, - workspaceSlug && projectId && issueId - ? () => issueService.getIssueActivities(workspaceSlug.toString(), projectId.toString(), issueId.toString()) - : null - ); - - const handleCommentUpdate = async (commentId: string, data: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - await issueCommentService - .patchIssueComment(workspaceSlug as string, projectId as string, issueId as string, commentId, data) - .then((res) => { - mutateIssueActivity(); - postHogEventTracker( - "COMMENT_UPDATED", - { - ...res, - state: "SUCCESS", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - }); - }; - - const handleCommentDelete = async (commentId: string) => { - if (!workspaceSlug || !projectId || !issueId || !currentUser) return; - - mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false); - - await issueCommentService - .deleteIssueComment(workspaceSlug as string, projectId as string, issueId as string, commentId) - .then(() => { - mutateIssueActivity(); - postHogEventTracker( - "COMMENT_DELETED", - { - state: "SUCCESS", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - }); - }; - - const handleAddComment = async (formData: IIssueActivity) => { - if (!workspaceSlug || !issueDetails || !currentUser) return; - - await issueCommentService - .createIssueComment(workspaceSlug.toString(), issueDetails.project_id, issueDetails.id, formData) - .then((res) => { - mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id)); - postHogEventTracker( - "COMMENT_ADDED", - { - ...res, - state: "SUCCESS", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Comment could not be posted. Please try again.", - }) - ); - }; - - const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const parentDetail = issueMap?.[issueDetails.parent_id || ""] || undefined; - - return ( - <> -
- {issueDetails?.parent_id && parentDetail ? ( -
- -
-
- state?.id === parentDetail?.state_id - )?.color, - }} - /> - - {getProjectById(parentDetail?.project_id)?.identifier}-{parentDetail?.sequence_id} - -
- {(parentDetail?.name ?? "").substring(0, 50)} -
- - - - {siblingIssuesList ? ( - siblingIssuesList.length > 0 ? ( - <> -

- Sibling issues -

- {siblingIssuesList.map((issue) => ( - - router.push(`/${workspaceSlug}/projects/${projectId as string}/issues/${issue.id}`) - } - className="flex items-center gap-2 py-2" - > - - {getProjectById(issueDetails?.project_id)?.identifier}-{issue.sequence_id} - - ))} - - ) : ( -

- No sibling issues -

- ) - ) : null} - submitChanges({ parent_id: null })} - className="flex items-center gap-2 py-2 text-red-500" - > - - Remove Parent Issue - -
-
- ) : null} - -
- {currentIssueState && ( - - )} - -
- - setIsSubmitting(value)} - isSubmitting={isSubmitting} - workspaceSlug={workspaceSlug as string} - issue={issueDetails} - handleFormSubmit={submitChanges} - isAllowed={isAllowed || !uneditable} - /> - - {workspaceSlug && projectId && ( - - )} - -
- -
-
- - {/* issue attachments */} - - -
-

Comments/Activity

- - -
- - ); -}); diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 3eb7037f2..8a0ab0fe7 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -1,133 +1,32 @@ -import { ChangeEvent, FC, useCallback, useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import debounce from "lodash/debounce"; -// packages -import { RichTextEditor } from "@plane/rich-text-editor"; +import { FC } from "react"; // hooks -import { useMention, useProject, useUser } from "hooks/store"; -import useReloadConfirmations from "hooks/use-reload-confirmation"; +import { useIssueDetail, useProject, useUser } from "hooks/store"; // components -import { IssuePeekOverviewReactions } from "components/issues"; -// ui -import { TextArea } from "@plane/ui"; -// types -import { TIssue, IUser } from "@plane/types"; -// services -import { FileService } from "services/file.service"; -// constants -import { EUserProjectRoles } from "constants/project"; - -const fileService = new FileService(); +import { IssueDescriptionForm, TIssueOperations } from "components/issues"; +import { IssueReaction } from "../issue-detail/reactions"; interface IPeekOverviewIssueDetails { workspaceSlug: string; - issue: TIssue; - issueReactions: any; - user: IUser | null; - issueUpdate: (issue: Partial) => void; - issueReactionCreate: (reaction: string) => void; - issueReactionRemove: (reaction: string) => void; + projectId: string; + issueId: string; + issueOperations: TIssueOperations; + is_archived: boolean; + disabled: boolean; isSubmitting: "submitting" | "submitted" | "saved"; setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } export const PeekOverviewIssueDetails: FC = (props) => { - const { - workspaceSlug, - issue, - issueReactions, - user, - issueUpdate, - issueReactionCreate, - issueReactionRemove, - isSubmitting, - setIsSubmitting, - } = props; - // states - const [characterLimit, setCharacterLimit] = useState(false); + const { workspaceSlug, projectId, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); - const { mentionHighlights, mentionSuggestions } = useMention(); const { getProjectById } = useProject(); - // derived values - const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - // toast alert - const { setShowAlert } = useReloadConfirmations(); - // form info + const { currentUser } = useUser(); const { - handleSubmit, - watch, - reset, - control, - formState: { errors }, - } = useForm({ - defaultValues: { - name: issue.name, - description_html: issue.description_html, - }, - }); - - const handleDescriptionFormSubmit = useCallback( - async (formData: Partial) => { - if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; - - await issueUpdate({ - ...issue, - name: formData.name ?? "", - description_html: formData.description_html ?? "

", - }); - }, - [issue, issueUpdate] - ); - - const [localTitleValue, setLocalTitleValue] = useState(""); - const [localIssueDescription, setLocalIssueDescription] = useState({ - id: issue.id, - description_html: issue.description_html, - }); - - // adding issue.description_html or issue.name to dependency array causes - // editor rerendering on every save - useEffect(() => { - if (issue.id) { - setLocalIssueDescription({ id: issue.id, description_html: issue.description_html }); - setLocalTitleValue(issue.name); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [issue.id]); // TODO: Verify the exhaustive-deps warning - - // ADDING handleDescriptionFormSubmit TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS - // TODO: Verify the exhaustive-deps warning - // eslint-disable-next-line react-hooks/exhaustive-deps - const debouncedFormSave = useCallback( - debounce(async () => { - handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted")); - }, 1500), - [handleSubmit] - ); - - useEffect(() => { - if (isSubmitting === "submitted") { - setShowAlert(false); - setTimeout(async () => { - setIsSubmitting("saved"); - }, 2000); - } else if (isSubmitting === "submitting") { - setShowAlert(true); - } - }, [isSubmitting, setShowAlert, setIsSubmitting]); - - // reset form values - useEffect(() => { - if (!issue) return; - - reset({ - ...issue, - }); - }, [issue, reset]); - + issue: { getIssueById }, + } = useIssueDetail(); + // derived values + const issue = getIssueById(issueId); + if (!issue) return <>; const projectDetails = getProjectById(issue?.project_id); return ( @@ -135,82 +34,24 @@ export const PeekOverviewIssueDetails: FC = (props) = {projectDetails?.identifier}-{issue?.sequence_id} - -
- {isAllowed ? ( - ( -