diff --git a/.eslintrc.js b/.eslintrc.js index 463c86901..c229c0952 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { extends: ["custom"], settings: { next: { - rootDir: ["apps/*"], + rootDir: ["web/", "space/"], }, }, }; diff --git a/.github/workflows/Build_Test_Pull_Request.yml b/.github/workflows/Build_Test_Pull_Request.yml index 0dbca646a..438bdbef3 100644 --- a/.github/workflows/Build_Test_Pull_Request.yml +++ b/.github/workflows/Build_Test_Pull_Request.yml @@ -29,9 +29,9 @@ jobs: apiserver: - apiserver/** web: - - apps/app/** + - web/** deploy: - - apps/space/** + - space/** - name: Setup .npmrc for repository run: | @@ -40,15 +40,15 @@ jobs: - name: Build Plane's Main App if: steps.changed-files.outputs.web_any_changed == 'true' run: | - mv ./.npmrc ./apps/app - cd apps/app + mv ./.npmrc ./web + cd web yarn yarn build - name: Build Plane's Deploy App if: steps.changed-files.outputs.deploy_any_changed == 'true' run: | - cd apps/space + cd space yarn yarn build diff --git a/.github/workflows/Update_Docker_Images.yml b/.github/workflows/Update_Docker_Images.yml index 8e27e098f..64b7eb085 100644 --- a/.github/workflows/Update_Docker_Images.yml +++ b/.github/workflows/Update_Docker_Images.yml @@ -2,7 +2,7 @@ name: Update Docker Images for Plane on Release on: release: - types: [released] + types: [released, prereleased] jobs: build_push_backend: @@ -62,7 +62,7 @@ jobs: uses: docker/build-push-action@v4.0.0 with: context: . - file: ./apps/app/Dockerfile.web + file: ./web/Dockerfile.web platforms: linux/amd64 tags: ${{ steps.metaFrontend.outputs.tags }} push: true @@ -88,7 +88,7 @@ jobs: uses: docker/build-push-action@v4.0.0 with: context: . - file: ./apps/space/Dockerfile.space + file: ./space/Dockerfile.space platforms: linux/amd64 push: true tags: ${{ steps.metaDeploy.outputs.tags }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 294dc1c0e..cd74b6121 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within @@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity). For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. \ No newline at end of file +https://www.contributor-covenant.org/translations. diff --git a/README.md b/README.md index 2bc2764f3..a5a7ddd87 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,10 @@ Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘‍♀️. - > Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases. The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting). - ## ⚡️ Quick start with Docker Compose ### Docker Compose Setup @@ -56,7 +54,7 @@ chmod +x setup.sh - Run setup.sh ```bash -./setup.sh http://localhost +./setup.sh http://localhost ``` > If running in a cloud env replace localhost with public facing IP address of the VM @@ -65,31 +63,32 @@ chmod +x setup.sh Visit [Tiptap Pro](https://collab.tiptap.dev/pro-extensions) and signup (it is free). - Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro. + Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro. ``` @tiptap-pro:registry=https://registry.tiptap.dev/ //registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN ``` + - Run Docker compose up ```bash docker compose up -d ``` -You can use the default email and password for your first login `captain@plane.so` and `password123`. +You can use the default email and password for your first login `captain@plane.so` and `password123`. ## 🚀 Features -* **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking. -* **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents. -* **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you. -* **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features. -* **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress. -* **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks. -* **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues. -* **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location. -* **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration. +- **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking. +- **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents. +- **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you. +- **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features. +- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress. +- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks. +- **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues. +- **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location. +- **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration. ## 📸 Screenshots @@ -150,7 +149,6 @@ docker compose up -d

- ## 📚Documentation For full documentation, visit [docs.plane.so](https://docs.plane.so/) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 4992c2f31..5888b759c 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -437,6 +437,9 @@ class IssueAttachmentSerializer(BaseSerializer): class IssueReactionSerializer(BaseSerializer): + + actor_detail = UserLiteSerializer(read_only=True, source="actor") + class Meta: model = IssueReaction fields = "__all__" @@ -448,19 +451,6 @@ class IssueReactionSerializer(BaseSerializer): ] -class IssueReactionLiteSerializer(BaseSerializer): - actor_detail = UserLiteSerializer(read_only=True, source="actor") - - class Meta: - model = IssueReaction - fields = [ - "id", - "reaction", - "issue", - "actor_detail", - ] - - class CommentReactionLiteSerializer(BaseSerializer): actor_detail = UserLiteSerializer(read_only=True, source="actor") @@ -482,9 +472,12 @@ class CommentReactionSerializer(BaseSerializer): class IssueVoteSerializer(BaseSerializer): + + actor_detail = UserLiteSerializer(read_only=True, source="actor") + class Meta: model = IssueVote - fields = ["issue", "vote", "workspace_id", "project_id", "actor"] + fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"] read_only_fields = fields @@ -554,7 +547,7 @@ class IssueSerializer(BaseSerializer): issue_link = IssueLinkSerializer(read_only=True, many=True) issue_attachment = IssueAttachmentSerializer(read_only=True, many=True) sub_issues_count = serializers.IntegerField(read_only=True) - issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) + issue_reactions = IssueReactionSerializer(read_only=True, many=True) class Meta: model = Issue @@ -580,7 +573,7 @@ class IssueLiteSerializer(BaseSerializer): module_id = serializers.UUIDField(read_only=True) attachment_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True) - issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) + issue_reactions = IssueReactionSerializer(read_only=True, many=True) class Meta: model = Issue @@ -601,7 +594,7 @@ class IssueLiteSerializer(BaseSerializer): class IssuePublicSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state") - reactions = IssueReactionLiteSerializer(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: diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 2d5f16ccd..b10689843 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -51,6 +51,7 @@ from plane.api.views import ( WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + LeaveWorkspaceEndpoint, ## End Workspaces # File Assets FileAssetEndpoint, @@ -68,6 +69,7 @@ from plane.api.views import ( UserProjectInvitationsViewset, ProjectIdentifierEndpoint, ProjectFavoritesViewSet, + LeaveProjectEndpoint, ## End Projects # Issues IssueViewSet, @@ -442,6 +444,11 @@ urlpatterns = [ WorkspaceLabelsEndpoint.as_view(), name="workspace-labels", ), + path( + "workspaces//members/leave/", + LeaveWorkspaceEndpoint.as_view(), + name="workspace-labels", + ), ## End Workspaces ## # Projects path( @@ -555,6 +562,11 @@ urlpatterns = [ ), name="project", ), + path( + "workspaces//projects//members/leave/", + LeaveProjectEndpoint.as_view(), + name="project", + ), # End Projects # States path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index fdf47cd5e..8e5f279a8 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -16,6 +16,7 @@ from .project import ( ProjectDeployBoardPublicSettingsEndpoint, ProjectMemberEndpoint, WorkspaceProjectDeployBoardEndpoint, + LeaveProjectEndpoint, ) from .user import ( UserEndpoint, @@ -52,6 +53,7 @@ from .workspace import ( WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, WorkspaceMembersEndpoint, + LeaveWorkspaceEndpoint, ) from .state import StateViewSet from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index a3d89fa81..253da2c5b 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -191,11 +191,10 @@ class CycleViewSet(BaseViewSet): workspace__slug=slug, project_id=project_id, ) - .annotate(first_name=F("assignees__first_name")) - .annotate(last_name=F("assignees__last_name")) + .annotate(display_name=F("assignees__display_name")) .annotate(assignee_id=F("assignees__id")) .annotate(avatar=F("assignees__avatar")) - .values("first_name", "last_name", "assignee_id", "avatar") + .values("display_name", "assignee_id", "avatar") .annotate(total_issues=Count("assignee_id")) .annotate( completed_issues=Count( @@ -209,7 +208,7 @@ class CycleViewSet(BaseViewSet): filter=Q(completed_at__isnull=True), ) ) - .order_by("first_name", "last_name") + .order_by("display_name") ) label_distribution = ( @@ -334,13 +333,21 @@ class CycleViewSet(BaseViewSet): 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(): - return Response( - { - "error": "The Cycle has already been completed so it cannot be edited" - }, - status=status.HTTP_400_BAD_REQUEST, - ) + if "sort_order" in request_data: + # Can only change sort order + request_data = { + "sort_order": request_data.get("sort_order", cycle.sort_order) + } + else: + return Response( + { + "error": "The Cycle has already been completed so it cannot be edited" + }, + status=status.HTTP_400_BAD_REQUEST, + ) serializer = CycleWriteSerializer(cycle, data=request.data, partial=True) if serializer.is_valid(): @@ -374,7 +381,9 @@ class CycleViewSet(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")) .annotate( completed_issues=Count( @@ -710,7 +719,6 @@ class CycleDateCheckEndpoint(BaseAPIView): class CycleFavoriteViewSet(BaseViewSet): - serializer_class = CycleFavoriteSerializer model = CycleFavorite diff --git a/apiserver/plane/api/views/gpt.py b/apiserver/plane/api/views/gpt.py index f8065f6d0..63c3f4f18 100644 --- a/apiserver/plane/api/views/gpt.py +++ b/apiserver/plane/api/views/gpt.py @@ -41,9 +41,9 @@ class GPTIntegrationEndpoint(BaseAPIView): final_text = task + "\n" + prompt openai.api_key = settings.OPENAI_API_KEY - response = openai.Completion.create( + response = openai.ChatCompletion.create( model=settings.GPT_ENGINE, - prompt=final_text, + messages=[{"role": "user", "content": final_text}], temperature=0.7, max_tokens=1024, ) @@ -51,7 +51,7 @@ class GPTIntegrationEndpoint(BaseAPIView): workspace = Workspace.objects.get(slug=slug) project = Project.objects.get(pk=project_id) - text = response.choices[0].text.strip() + text = response.choices[0].message.content.strip() text_html = text.replace("\n", "
") return Response( { diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index a5b5bc35f..0194863f2 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -17,11 +17,12 @@ from django.db.models import ( When, Exists, Max, + IntegerField, ) from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from django.db.models.functions import Coalesce +from django.db import IntegrityError from django.conf import settings from django.db import IntegrityError @@ -340,7 +341,11 @@ class UserWorkSpaceIssues(BaseAPIView): issue_queryset = ( Issue.issue_objects.filter( - (Q(assignees__in=[request.user]) | Q(created_by=request.user) | Q(issue_subscribers__subscriber=request.user)), + ( + Q(assignees__in=[request.user]) + | Q(created_by=request.user) + | Q(issue_subscribers__subscriber=request.user) + ), workspace__slug=slug, ) .annotate( @@ -1548,32 +1553,35 @@ class IssueCommentPublicViewSet(BaseViewSet): return super(IssueCommentPublicViewSet, self).get_permissions() def get_queryset(self): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.comments: - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter(access="EXTERNAL") - .select_related("project") - .select_related("workspace") - .select_related("issue") - .annotate( - is_member=Exists( - ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - member_id=self.request.user.id, + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.comments: + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(access="EXTERNAL") + .select_related("project") + .select_related("workspace") + .select_related("issue") + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + member_id=self.request.user.id, + ) ) ) - ) - .distinct() - ) - else: + .distinct() + ).order_by("created_at") + else: + return IssueComment.objects.none() + except ProjectDeployBoard.DoesNotExist: return IssueComment.objects.none() def create(self, request, slug, project_id, issue_id): @@ -1706,21 +1714,24 @@ class IssueReactionPublicViewSet(BaseViewSet): model = IssueReaction def get_queryset(self): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.reactions: - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .order_by("-created_at") - .distinct() + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), ) - else: + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .order_by("-created_at") + .distinct() + ) + else: + return IssueReaction.objects.none() + except ProjectDeployBoard.DoesNotExist: return IssueReaction.objects.none() def create(self, request, slug, project_id, issue_id): @@ -1821,21 +1832,24 @@ class CommentReactionPublicViewSet(BaseViewSet): model = CommentReaction def get_queryset(self): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.reactions: - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(comment_id=self.kwargs.get("comment_id")) - .order_by("-created_at") - .distinct() + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), ) - else: + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(comment_id=self.kwargs.get("comment_id")) + .order_by("-created_at") + .distinct() + ) + else: + return CommentReaction.objects.none() + except ProjectDeployBoard.DoesNotExist: return CommentReaction.objects.none() def create(self, request, slug, project_id, comment_id): @@ -1942,13 +1956,23 @@ class IssueVotePublicViewSet(BaseViewSet): serializer_class = IssueVoteSerializer def get_queryset(self): - return ( - super() - .get_queryset() - .filter(issue_id=self.kwargs.get("issue_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - ) + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.votes: + return ( + super() + .get_queryset() + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + ) + else: + return IssueVote.objects.none() + except ProjectDeployBoard.DoesNotExist: + return IssueVote.objects.none() def create(self, request, slug, project_id, issue_id): try: @@ -1977,6 +2001,10 @@ class IssueVotePublicViewSet(BaseViewSet): ) serializer = IssueVoteSerializer(issue_vote) return Response(serializer.data, status=status.HTTP_201_CREATED) + except IntegrityError: + return Response( + {"error": "Reaction already exists"}, status=status.HTTP_400_BAD_REQUEST + ) except Exception as e: capture_exception(e) return Response( @@ -2167,6 +2195,12 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): queryset=IssueReaction.objects.select_related("actor"), ) ) + .prefetch_related( + Prefetch( + "votes", + queryset=IssueVote.objects.select_related("actor"), + ) + ) .filter(**filters) .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate(module_id=F("issue_module__module_id")) @@ -2246,9 +2280,33 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): issues = IssuePublicSerializer(issue_queryset, many=True).data - states = State.objects.filter( - workspace__slug=slug, project_id=project_id - ).values("name", "group", "color", "id") + state_group_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + states = ( + State.objects.filter( + ~Q(name="Triage"), + workspace__slug=slug, + project_id=project_id, + ) + .annotate( + custom_order=Case( + *[ + When(group=value, then=Value(index)) + for index, value in enumerate(state_group_order) + ], + default=Value(len(state_group_order)), + output_field=IntegerField(), + ), + ) + .values("name", "group", "color", "id") + .order_by("custom_order", "sequence") + ) labels = Label.objects.filter( workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 97b06fce5..093c8ff78 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -11,14 +11,8 @@ from django.db.models import ( OuterRef, Func, F, - Max, - CharField, Func, Subquery, - Prefetch, - When, - Case, - Value, ) from django.core.validators import validate_email from django.conf import settings @@ -47,6 +41,7 @@ from plane.api.permissions import ( ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, + ProjectLitePermission, ) from plane.db.models import ( @@ -71,16 +66,9 @@ from plane.db.models import ( ModuleMember, Inbox, ProjectDeployBoard, - Issue, - IssueReaction, - IssueLink, - IssueAttachment, - Label, ) from plane.bgtasks.project_invitation_task import project_invitation -from plane.utils.grouper import group_results -from plane.utils.issue_filters import issue_filters class ProjectViewSet(BaseViewSet): @@ -494,7 +482,7 @@ class UserProjectInvitationsViewset(BaseViewSet): # Delete joined project invites project_invitations.delete() - return Response(status=status.HTTP_200_OK) + return Response(status=status.HTTP_204_NO_CONTENT) except Exception as e: capture_exception(e) return Response( @@ -629,7 +617,7 @@ class ProjectMemberViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) except ProjectMember.DoesNotExist: return Response( - {"error": "Project Member does not exist"}, status=status.HTTP_400 + {"error": "Project Member does not exist"}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: capture_exception(e) @@ -936,8 +924,7 @@ class ProjectUserViewsEndpoint(BaseAPIView): project_member.save() - return Response(status=status.HTTP_200_OK) - + return Response(status=status.HTTP_204_NO_CONTENT) except Project.DoesNotExist: return Response( {"error": "The requested resource does not exists"}, @@ -1144,8 +1131,9 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): - - permission_classes = [AllowAny,] + permission_classes = [ + AllowAny, + ] def get(self, request, slug): try: @@ -1176,3 +1164,48 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class LeaveProjectEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] + + def delete(self, request, slug, project_id): + try: + project_member = ProjectMember.objects.get( + workspace__slug=slug, + member=request.user, + project_id=project_id, + ) + + # Only Admin case + if ( + project_member.role == 20 + and ProjectMember.objects.filter( + workspace__slug=slug, + role=20, + project_id=project_id, + ).count() + == 1 + ): + return Response( + { + "error": "You cannot leave the project since you are the only admin of the project you should delete the project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Delete the member from workspace + project_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except ProjectMember.DoesNotExist: + return Response( + {"error": "Workspace member does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index cbf62548f..2ec3f324a 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -116,7 +116,7 @@ class WorkSpaceViewSet(BaseViewSet): ) issue_count = ( - Issue.objects.filter(workspace=OuterRef("id")) + Issue.issue_objects.filter(workspace=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -203,7 +203,7 @@ class UserWorkSpacesEndpoint(BaseAPIView): ) issue_count = ( - Issue.objects.filter(workspace=OuterRef("id")) + Issue.issue_objects.filter(workspace=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -532,7 +532,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet): # Delete joined workspace invites workspace_invitations.delete() - return Response(status=status.HTTP_200_OK) + return Response(status=status.HTTP_204_NO_CONTENT) except Exception as e: capture_exception(e) return Response( @@ -846,7 +846,7 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView): workspace_member.view_props = request.data.get("view_props", {}) workspace_member.save() - return Response(status=status.HTTP_200_OK) + return Response(status=status.HTTP_204_NO_CONTENT) except WorkspaceMember.DoesNotExist: return Response( {"error": "User not a member of workspace"}, @@ -1075,7 +1075,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView): priority_order = ["urgent", "high", "medium", "low", None] priority_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( workspace__slug=slug, assignees__in=[user_id], project__project_projectmember__member=request.user, @@ -1473,3 +1473,44 @@ class WorkspaceMembersEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class LeaveWorkspaceEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def delete(self, request, slug): + try: + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + + # Only Admin case + if ( + workspace_member.role == 20 + and WorkspaceMember.objects.filter( + workspace__slug=slug, role=20 + ).count() + == 1 + ): + return Response( + { + "error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Delete the member from workspace + workspace_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except WorkspaceMember.DoesNotExist: + return Response( + {"error": "Workspace member does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index a1f4a3e92..645772c94 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -32,7 +32,7 @@ def archive_old_issues(): archive_in = project.archive_in # Get all the issues whose updated_at in less that the archive_in month - issues = Issue.objects.filter( + issues = Issue.issue_objects.filter( Q( project=project_id, archived_at__isnull=True, @@ -64,21 +64,22 @@ def archive_old_issues(): issues_to_update.append(issue) # Bulk Update the issues and log the activity - updated_issues = Issue.objects.bulk_update( - issues_to_update, ["archived_at"], batch_size=100 - ) - [ - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"archived_at": str(issue.archived_at)}), - actor_id=str(project.created_by_id), - issue_id=issue.id, - project_id=project_id, - current_instance=None, - subscriber=False, + if issues_to_update: + updated_issues = Issue.objects.bulk_update( + issues_to_update, ["archived_at"], batch_size=100 ) - for issue in updated_issues - ] + [ + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"archived_at": str(issue.archived_at)}), + actor_id=str(project.created_by_id), + issue_id=issue.id, + project_id=project_id, + current_instance=None, + subscriber=False, + ) + for issue in updated_issues + ] return except Exception as e: if settings.DEBUG: @@ -99,7 +100,7 @@ def close_old_issues(): close_in = project.close_in # Get all the issues whose updated_at in less that the close_in month - issues = Issue.objects.filter( + issues = Issue.issue_objects.filter( Q( project=project_id, archived_at__isnull=True, @@ -136,19 +137,20 @@ def close_old_issues(): issues_to_update.append(issue) # Bulk Update the issues and log the activity - updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) - [ - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"closed_to": str(issue.state_id)}), - actor_id=str(project.created_by_id), - issue_id=issue.id, - project_id=project_id, - current_instance=None, - subscriber=False, - ) - for issue in updated_issues - ] + if issues_to_update: + updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) + [ + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"closed_to": str(issue.state_id)}), + actor_id=str(project.created_by_id), + issue_id=issue.id, + project_id=project_id, + current_instance=None, + subscriber=False, + ) + for issue in updated_issues + ] return except Exception as e: if settings.DEBUG: 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 f7d6a979d..01af46d20 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 @@ -3,7 +3,7 @@ from django.conf import settings 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") @@ -31,5 +31,30 @@ class Migration(migrations.Migration): name='title', field=models.CharField(blank=True, max_length=255, null=True), ), - migrations.RunPython(update_user_timezones) + migrations.RunPython(update_user_timezones), + migrations.AlterField( + model_name='issuevote', + name='vote', + field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1), + ), + migrations.CreateModel( + 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')), + ], + options={ + '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/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py b/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py deleted file mode 100644 index d8063acc0..000000000 --- a/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.2.3 on 2023-08-29 07:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('db', '0041_cycle_sort_order_issuecomment_access_and_more'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='issuevote', - unique_together=set(), - ), - migrations.AlterField( - model_name='issuevote', - name='vote', - field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1), - ), - migrations.AlterUniqueTogether( - name='issuevote', - unique_together={('issue', 'actor', 'vote')}, - ), - ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 143576fb1..0d079fe32 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -512,7 +512,7 @@ class IssueVote(ProjectBaseModel): ) class Meta: - unique_together = ["issue", "actor", "vote"] + unique_together = ["issue", "actor",] verbose_name = "Issue Vote" verbose_name_plural = "Issue Votes" db_table = "issue_votes" diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index 033452e0d..60e751459 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -96,7 +96,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): chart_data = {str(date): 0 for date in date_range} completed_issues_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( workspace__slug=slug, project_id=project_id, issue_cycle__cycle_id=cycle_id, @@ -118,7 +118,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): chart_data = {str(date): 0 for date in date_range} completed_issues_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( workspace__slug=slug, project_id=project_id, issue_module__module_id=module_id, diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index ca9d881ef..969ab3c89 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,36 +1,36 @@ # base requirements -Django==4.2.3 +Django==4.2.5 django-braces==1.15.0 django-taggit==4.0.0 -psycopg==3.1.9 +psycopg==3.1.10 django-oauth-toolkit==2.3.0 mistune==3.0.1 djangorestframework==3.14.0 redis==4.6.0 django-nested-admin==4.0.2 -django-cors-headers==4.1.0 +django-cors-headers==4.2.0 whitenoise==6.5.0 -django-allauth==0.54.0 +django-allauth==0.55.2 faker==18.11.2 django-filter==23.2 jsonmodels==2.6.0 -djangorestframework-simplejwt==5.2.2 -sentry-sdk==1.27.0 +djangorestframework-simplejwt==5.3.0 +sentry-sdk==1.30.0 django-s3-storage==0.14.0 django-crum==0.7.9 django-guardian==2.4.0 dj_rest_auth==2.2.5 -google-auth==2.21.0 -google-api-python-client==2.92.0 +google-auth==2.22.0 +google-api-python-client==2.97.0 django-redis==5.3.0 -uvicorn==0.22.0 +uvicorn==0.23.2 channels==4.0.0 -openai==0.27.8 +openai==0.28.0 slack-sdk==3.21.3 -celery==5.3.1 +celery==5.3.4 django_celery_beat==2.5.0 -psycopg-binary==3.1.9 -psycopg-c==3.1.9 +psycopg-binary==3.1.10 +psycopg-c==3.1.10 scout-apm==2.26.1 openpyxl==3.1.2 \ No newline at end of file diff --git a/apiserver/requirements/production.txt b/apiserver/requirements/production.txt index 4da619d49..5e3483a96 100644 --- a/apiserver/requirements/production.txt +++ b/apiserver/requirements/production.txt @@ -1,11 +1,11 @@ -r base.txt -dj-database-url==2.0.0 -gunicorn==20.1.0 +dj-database-url==2.1.0 +gunicorn==21.2.0 whitenoise==6.5.0 -django-storages==1.13.2 -boto3==1.27.0 -django-anymail==10.0 +django-storages==1.14 +boto3==1.28.40 +django-anymail==10.1 django-debug-toolbar==4.1.0 gevent==23.7.0 psycogreen==1.0.2 \ No newline at end of file diff --git a/apps/app/components/gantt-chart/helpers/block-structure.tsx b/apps/app/components/gantt-chart/helpers/block-structure.tsx deleted file mode 100644 index ab2475bdd..000000000 --- a/apps/app/components/gantt-chart/helpers/block-structure.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// types -import { IIssue } from "types"; -import { IGanttBlock } from "components/gantt-chart"; - -export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] => - blocks && blocks.length > 0 - ? blocks.map((block) => ({ - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: new Date(block.start_date ?? ""), - target_date: new Date(block.target_date ?? ""), - })) - : []; diff --git a/apps/app/components/icons/backlog-state-icon.tsx b/apps/app/components/icons/backlog-state-icon.tsx deleted file mode 100644 index 2c140a112..000000000 --- a/apps/app/components/icons/backlog-state-icon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const BacklogStateIcon: React.FC = ({ - width = "20", - height = "20", - className, - color = "rgb(var(--color-text-200))", -}) => ( - - - -); diff --git a/apps/app/components/icons/blocked-icon.tsx b/apps/app/components/icons/blocked-icon.tsx deleted file mode 100644 index ee0024fa0..000000000 --- a/apps/app/components/icons/blocked-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const BlockedIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - - ); diff --git a/apps/app/components/icons/blocker-icon.tsx b/apps/app/components/icons/blocker-icon.tsx deleted file mode 100644 index 093728cd8..000000000 --- a/apps/app/components/icons/blocker-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const BlockerIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - - ); diff --git a/apps/app/components/icons/bolt-icon.tsx b/apps/app/components/icons/bolt-icon.tsx deleted file mode 100644 index 569767aa5..000000000 --- a/apps/app/components/icons/bolt-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const BoltIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/cancel-icon.tsx b/apps/app/components/icons/cancel-icon.tsx deleted file mode 100644 index c3170ca32..000000000 --- a/apps/app/components/icons/cancel-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CancelIcon: React.FC = ({ width, height, className }) => ( - - - - ); diff --git a/apps/app/components/icons/cancelled-state-icon.tsx b/apps/app/components/icons/cancelled-state-icon.tsx deleted file mode 100644 index 5829146ff..000000000 --- a/apps/app/components/icons/cancelled-state-icon.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CancelledStateIcon: React.FC = ({ - width = "20", - height = "20", - className, - color = "#f2655a", -}) => ( - - - - - - - - - - - - - -); diff --git a/apps/app/components/icons/clipboard-icon.tsx b/apps/app/components/icons/clipboard-icon.tsx deleted file mode 100644 index c96aa3fde..000000000 --- a/apps/app/components/icons/clipboard-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const ClipboardIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/comment-icon.tsx b/apps/app/components/icons/comment-icon.tsx deleted file mode 100644 index c60cca4a6..000000000 --- a/apps/app/components/icons/comment-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CommentIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/completed-cycle-icon.tsx b/apps/app/components/icons/completed-cycle-icon.tsx deleted file mode 100644 index 615fbcb9a..000000000 --- a/apps/app/components/icons/completed-cycle-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CompletedCycleIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( - - - - ); diff --git a/apps/app/components/icons/completed-state-icon.tsx b/apps/app/components/icons/completed-state-icon.tsx deleted file mode 100644 index 584245d58..000000000 --- a/apps/app/components/icons/completed-state-icon.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CompletedStateIcon: React.FC = ({ - width = "20", - height = "20", - className, - color = "#438af3", -}) => ( - - - - - - - - - - - - -); diff --git a/apps/app/components/icons/current-cycle-icon.tsx b/apps/app/components/icons/current-cycle-icon.tsx deleted file mode 100644 index 2b07edf2e..000000000 --- a/apps/app/components/icons/current-cycle-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CurrentCycleIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( - - - - ); diff --git a/apps/app/components/icons/edit-icon.tsx b/apps/app/components/icons/edit-icon.tsx deleted file mode 100644 index c4e012e4d..000000000 --- a/apps/app/components/icons/edit-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const EditIcon: React.FC = ({ width, height, className }) => ( - - - - ); diff --git a/apps/app/components/icons/ellipsis-horizontal-icon.tsx b/apps/app/components/icons/ellipsis-horizontal-icon.tsx deleted file mode 100644 index cfdd66751..000000000 --- a/apps/app/components/icons/ellipsis-horizontal-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const EllipsisHorizontalIcon: React.FC = ({ width, height, className }) => ( - - - - ); diff --git a/apps/app/components/icons/lock-icon.tsx b/apps/app/components/icons/lock-icon.tsx deleted file mode 100644 index d0c9cffb7..000000000 --- a/apps/app/components/icons/lock-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const LockIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/menu-icon.tsx b/apps/app/components/icons/menu-icon.tsx deleted file mode 100644 index 0a8816b75..000000000 --- a/apps/app/components/icons/menu-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const MenuIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/plus-icon.tsx b/apps/app/components/icons/plus-icon.tsx deleted file mode 100644 index 0b958a21d..000000000 --- a/apps/app/components/icons/plus-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const PlusIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/priority-icon.tsx b/apps/app/components/icons/priority-icon.tsx deleted file mode 100644 index 58212ca5a..000000000 --- a/apps/app/components/icons/priority-icon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export const getPriorityIcon = (priority: string | null, className?: string) => { - if (!className || className === "") className = "text-xs flex items-center"; - - priority = priority?.toLowerCase() ?? null; - - switch (priority) { - case "urgent": - return error; - case "high": - return signal_cellular_alt; - case "medium": - return ( - signal_cellular_alt_2_bar - ); - case "low": - return ( - signal_cellular_alt_1_bar - ); - default: - return block; - } -}; diff --git a/apps/app/components/icons/question-mark-circle-icon.tsx b/apps/app/components/icons/question-mark-circle-icon.tsx deleted file mode 100644 index 2cdf9d8e5..000000000 --- a/apps/app/components/icons/question-mark-circle-icon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const QuestionMarkCircleIcon: React.FC = ({ - width = "24", - height = "24", - className, -}) => ( - - - - ); diff --git a/apps/app/components/icons/signal-cellular-icon.tsx b/apps/app/components/icons/signal-cellular-icon.tsx deleted file mode 100644 index 0e785d958..000000000 --- a/apps/app/components/icons/signal-cellular-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const SignalCellularIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/started-state-icon.tsx b/apps/app/components/icons/started-state-icon.tsx deleted file mode 100644 index 20de01537..000000000 --- a/apps/app/components/icons/started-state-icon.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const StartedStateIcon: React.FC = ({ - width = "20", - height = "20", - className, - color = "#fbb040", -}) => ( - - - - - - - - - - - - -); diff --git a/apps/app/components/icons/state-group-icon.tsx b/apps/app/components/icons/state-group-icon.tsx deleted file mode 100644 index 522e0b9dc..000000000 --- a/apps/app/components/icons/state-group-icon.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - BacklogStateIcon, - CancelledStateIcon, - CompletedStateIcon, - StartedStateIcon, - UnstartedStateIcon, -} from "components/icons"; -// constants -import { STATE_GROUP_COLORS } from "constants/state"; - -export const getStateGroupIcon = ( - stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled", - width = "20", - height = "20", - color?: string -) => { - switch (stateGroup) { - case "backlog": - return ( - - ); - case "unstarted": - return ( - - ); - case "started": - return ( - - ); - case "completed": - return ( - - ); - case "cancelled": - return ( - - ); - default: - return <>; - } -}; diff --git a/apps/app/components/icons/tag-icon.tsx b/apps/app/components/icons/tag-icon.tsx deleted file mode 100644 index a17d4c1e4..000000000 --- a/apps/app/components/icons/tag-icon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const TagIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( - - - - ); diff --git a/apps/app/components/icons/transfer-icon.tsx b/apps/app/components/icons/transfer-icon.tsx deleted file mode 100644 index 176c38b29..000000000 --- a/apps/app/components/icons/transfer-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const TransferIcon: React.FC = ({ width, height, className, color }) => ( - - - - ); diff --git a/apps/app/components/icons/tune-icon.tsx b/apps/app/components/icons/tune-icon.tsx deleted file mode 100644 index 1221b2976..000000000 --- a/apps/app/components/icons/tune-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const TuneIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/unstarted-state-icon.tsx b/apps/app/components/icons/unstarted-state-icon.tsx deleted file mode 100644 index 161a0ab2a..000000000 --- a/apps/app/components/icons/unstarted-state-icon.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const UnstartedStateIcon: React.FC = ({ - width = "20", - height = "20", - className, - color = "rgb(var(--color-text-200))", -}) => ( - - - - - - - - - - -); diff --git a/apps/app/components/icons/upcoming-cycle-icon.tsx b/apps/app/components/icons/upcoming-cycle-icon.tsx deleted file mode 100644 index 52961e15e..000000000 --- a/apps/app/components/icons/upcoming-cycle-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const UpcomingCycleIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( - - - - ); diff --git a/apps/app/components/icons/user-icon-circle.tsx b/apps/app/components/icons/user-icon-circle.tsx deleted file mode 100644 index 8bae34133..000000000 --- a/apps/app/components/icons/user-icon-circle.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const UserCircleIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/user-icon.tsx b/apps/app/components/icons/user-icon.tsx deleted file mode 100644 index c0408dad3..000000000 --- a/apps/app/components/icons/user-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const UserIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/integration/slack/index.ts b/apps/app/components/integration/slack/index.ts deleted file mode 100644 index 3bd1c965c..000000000 --- a/apps/app/components/integration/slack/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./select-channel"; \ No newline at end of file diff --git a/apps/app/components/project/single-sidebar-project.tsx b/apps/app/components/project/single-sidebar-project.tsx deleted file mode 100644 index 6fbdbbaf0..000000000 --- a/apps/app/components/project/single-sidebar-project.tsx +++ /dev/null @@ -1,355 +0,0 @@ -import Link from "next/link"; -import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// react-beautiful-dnd -import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd"; -// headless ui -import { Disclosure, Transition } from "@headlessui/react"; -// services -import projectService from "services/project.service"; -// hooks -import useToast from "hooks/use-toast"; -// ui -import { CustomMenu, Icon, Tooltip } from "components/ui"; -// icons -import { EllipsisVerticalIcon, LinkIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { - ArchiveOutlined, - ArticleOutlined, - ContrastOutlined, - DatasetOutlined, - ExpandMoreOutlined, - FilterNoneOutlined, - PhotoFilterOutlined, - SettingsOutlined, -} from "@mui/icons-material"; -// helpers -import { renderEmoji } from "helpers/emoji.helper"; -// types -import { IProject } from "types"; -// fetch-keys -import { PROJECTS_LIST } from "constants/fetch-keys"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -type Props = { - project: IProject; - sidebarCollapse: boolean; - provided?: DraggableProvided; - snapshot?: DraggableStateSnapshot; - handleDeleteProject: () => void; - handleCopyText: () => void; - shortContextMenu?: boolean; -}; - -const navigation = (workspaceSlug: string, projectId: string) => [ - { - name: "Issues", - href: `/${workspaceSlug}/projects/${projectId}/issues`, - Icon: FilterNoneOutlined, - }, - { - name: "Cycles", - href: `/${workspaceSlug}/projects/${projectId}/cycles`, - Icon: ContrastOutlined, - }, - { - name: "Modules", - href: `/${workspaceSlug}/projects/${projectId}/modules`, - Icon: DatasetOutlined, - }, - { - name: "Views", - href: `/${workspaceSlug}/projects/${projectId}/views`, - Icon: PhotoFilterOutlined, - }, - { - name: "Pages", - href: `/${workspaceSlug}/projects/${projectId}/pages`, - Icon: ArticleOutlined, - }, - { - name: "Settings", - href: `/${workspaceSlug}/projects/${projectId}/settings`, - Icon: SettingsOutlined, - }, -]; - -export const SingleSidebarProject: React.FC = observer( - ({ - project, - sidebarCollapse, - provided, - snapshot, - handleDeleteProject, - handleCopyText, - shortContextMenu = false, - }) => { - const store: RootStore = useMobxStore(); - const { projectPublish } = store; - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { setToastAlert } = useToast(); - - const isAdmin = project.member_role === 20; - - const handleAddToFavorites = () => { - if (!workspaceSlug) return; - - mutate( - PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), - (prevData) => - (prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: true } : p)), - false - ); - - projectService - .addProjectToFavorites(workspaceSlug as string, { - project: project.id, - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }) - ); - }; - - const handleRemoveFromFavorites = () => { - if (!workspaceSlug) return; - - mutate( - PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), - (prevData) => - (prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: false } : p)), - false - ); - - projectService.removeProjectFromFavorites(workspaceSlug as string, project.id).catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }) - ); - }; - - return ( - - {({ open }) => ( - <> -
- {provided && ( - - - - )} - - -
- {project.emoji ? ( - - {renderEmoji(project.emoji)} - - ) : project.icon_prop ? ( -
- {renderEmoji(project.icon_prop)} -
- ) : ( - - {project?.name.charAt(0)} - - )} - - {!sidebarCollapse && ( -

- {project.name} -

- )} -
- {!sidebarCollapse && ( - - )} -
-
- - {!sidebarCollapse && ( - - {!shortContextMenu && isAdmin && ( - - - - Delete project - - - )} - {!project.is_favorite && ( - - - - Add to favorites - - - )} - {project.is_favorite && ( - - - - Remove from favorites - - - )} - - - - Copy project link - - - - {/* publish project settings */} - {isAdmin && ( - projectPublish.handleProjectModal(project?.id)} - > -
-
- -
-
{project.is_deployed ? "Publish settings" : "Publish"}
-
-
- )} - - {project.archive_in > 0 && ( - - router.push(`/${workspaceSlug}/projects/${project?.id}/archived-issues/`) - } - > -
- - Archived Issues -
-
- )} - - router.push(`/${workspaceSlug}/projects/${project?.id}/settings`) - } - > -
- - Settings -
-
-
- )} -
- - - - {navigation(workspaceSlug as string, project?.id).map((item) => { - if ( - (item.name === "Cycles" && !project.cycle_view) || - (item.name === "Modules" && !project.module_view) || - (item.name === "Views" && !project.issue_views_view) || - (item.name === "Pages" && !project.page_view) - ) - return; - - return ( - - - -
- - {!sidebarCollapse && item.name} -
-
-
- - ); - })} -
-
- - )} -
- ); - } -); diff --git a/apps/app/components/tiptap/extensions/index.tsx b/apps/app/components/tiptap/extensions/index.tsx deleted file mode 100644 index fa257b20a..000000000 --- a/apps/app/components/tiptap/extensions/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import StarterKit from "@tiptap/starter-kit"; -import HorizontalRule from "@tiptap/extension-horizontal-rule"; -import TiptapLink from "@tiptap/extension-link"; -import Placeholder from "@tiptap/extension-placeholder"; -import TiptapUnderline from "@tiptap/extension-underline"; -import TextStyle from "@tiptap/extension-text-style"; -import { Color } from "@tiptap/extension-color"; -import TaskItem from "@tiptap/extension-task-item"; -import TaskList from "@tiptap/extension-task-list"; -import { Markdown } from "tiptap-markdown"; -import Highlight from "@tiptap/extension-highlight"; -import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; -import { lowlight } from "lowlight/lib/core"; -import SlashCommand from "../slash-command"; -import { InputRule } from "@tiptap/core"; -import Gapcursor from '@tiptap/extension-gapcursor' - -import ts from "highlight.js/lib/languages/typescript"; - -import "highlight.js/styles/github-dark.css"; -import UniqueID from "@tiptap-pro/extension-unique-id"; -import UpdatedImage from "./updated-image"; -import isValidHttpUrl from "../bubble-menu/utils/link-validator"; -import { CustomTableCell } from "./table/table-cell"; -import { Table } from "./table/table"; -import { TableHeader } from "./table/table-header"; -import { TableRow } from "@tiptap/extension-table-row"; - -lowlight.registerLanguage("ts", ts); - -export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", - }, - }, - orderedList: { - HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", - }, - }, - listItem: { - HTMLAttributes: { - class: "leading-normal -mb-2", - }, - }, - blockquote: { - HTMLAttributes: { - class: "border-l-4 border-custom-border-300", - }, - }, - code: { - HTMLAttributes: { - class: - "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", - spellcheck: "false", - }, - }, - codeBlock: false, - horizontalRule: false, - dropcursor: { - color: "rgba(var(--color-text-100))", - width: 2, - }, - gapcursor: false, - }), - CodeBlockLowlight.configure({ - lowlight, - }), - HorizontalRule.extend({ - addInputRules() { - return [ - new InputRule({ - find: /^(?:---|—-|___\s|\*\*\*\s)$/, - handler: ({ state, range, commands }) => { - commands.splitBlock(); - - const attributes = {}; - const { tr } = state; - const start = range.from; - const end = range.to; - // @ts-ignore - tr.replaceWith(start - 1, end, this.type.create(attributes)); - }, - }), - ]; - }, - }).configure({ - HTMLAttributes: { - class: "mb-6 border-t border-custom-border-300", - }, - }), - Gapcursor, - TiptapLink.configure({ - protocols: ["http", "https"], - validate: (url) => isValidHttpUrl(url), - HTMLAttributes: { - class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", - }, - }), - UpdatedImage.configure({ - HTMLAttributes: { - class: "rounded-lg border border-custom-border-300", - }, - }), - Placeholder.configure({ - placeholder: ({ node }) => { - if (node.type.name === "heading") { - return `Heading ${node.attrs.level}`; - } - if (node.type.name === "image" || node.type.name === "table") { - return "" - } - - return "Press '/' for commands..."; - }, - includeChildren: true, - }), - UniqueID.configure({ - types: ["image"], - }), - SlashCommand(workspaceSlug, setIsSubmitting), - TiptapUnderline, - TextStyle, - Color, - Highlight.configure({ - multicolor: true, - }), - TaskList.configure({ - HTMLAttributes: { - class: "not-prose pl-2", - }, - }), - TaskItem.configure({ - HTMLAttributes: { - class: "flex items-start my-4", - }, - nested: true, - }), - Markdown.configure({ - html: true, - transformCopiedText: true, - }), - Table, - TableHeader, - CustomTableCell, - TableRow -]; diff --git a/apps/app/components/tiptap/plugins/delete-image.tsx b/apps/app/components/tiptap/plugins/delete-image.tsx deleted file mode 100644 index 262a3f591..000000000 --- a/apps/app/components/tiptap/plugins/delete-image.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Plugin, PluginKey } from "@tiptap/pm/state"; -import { Node as ProseMirrorNode } from '@tiptap/pm/model'; -import fileService from "services/file.service"; - -const deleteKey = new PluginKey("delete-image"); - -const TrackImageDeletionPlugin = () => - new Plugin({ - key: deleteKey, - appendTransaction: (transactions, oldState, newState) => { - transactions.forEach((transaction) => { - if (!transaction.docChanged) return; - - const removedImages: ProseMirrorNode[] = []; - - oldState.doc.descendants((oldNode, oldPos) => { - if (oldNode.type.name !== 'image') return; - - if (oldPos < 0 || oldPos > newState.doc.content.size) return; - if (!newState.doc.resolve(oldPos).parent) return; - const newNode = newState.doc.nodeAt(oldPos); - - // Check if the node has been deleted or replaced - if (!newNode || newNode.type.name !== 'image') { - // Check if the node still exists elsewhere in the document - let nodeExists = false; - newState.doc.descendants((node) => { - if (node.attrs.id === oldNode.attrs.id) { - nodeExists = true; - } - }); - if (!nodeExists) { - removedImages.push(oldNode as ProseMirrorNode); - } - } - }); - - removedImages.forEach((node) => { - const src = node.attrs.src; - onNodeDeleted(src); - }); - }); - - return null; - }, - }); - -export default TrackImageDeletionPlugin; - -async function onNodeDeleted(src: string) { - const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); - const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); - if (resStatus === 204) { - console.log("Image deleted successfully"); - } -} diff --git a/apps/app/components/workspace/issues-stats.tsx b/apps/app/components/workspace/issues-stats.tsx deleted file mode 100644 index 1b327227a..000000000 --- a/apps/app/components/workspace/issues-stats.tsx +++ /dev/null @@ -1,83 +0,0 @@ -// components -import { ActivityGraph } from "components/workspace"; -// ui -import { Loader, Tooltip } from "components/ui"; -// icons -import { InformationCircleIcon } from "@heroicons/react/24/outline"; -// types -import { IUserWorkspaceDashboard } from "types"; - -type Props = { - data: IUserWorkspaceDashboard | undefined; -}; - -export const IssuesStats: React.FC = ({ data }) => ( -
-
-
-
-

Issues assigned to you

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

Pending issues

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

Completed issues

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

Issues due by this week

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

- Activity Graph - - - -

- -
-
-); diff --git a/apps/space/.env.example b/apps/space/.env.example deleted file mode 100644 index 4fb0e4df6..000000000 --- a/apps/space/.env.example +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_API_BASE_URL='' \ No newline at end of file diff --git a/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx b/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx deleted file mode 100644 index 7b4ed6142..000000000 --- a/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx +++ /dev/null @@ -1,66 +0,0 @@ -// next imports -import Link from "next/link"; -import Image from "next/image"; -import { Metadata, ResolvingMetadata } from "next"; -// components -import IssueNavbar from "components/issues/navbar"; -import IssueFilter from "components/issues/filters-render"; -// service -import ProjectService from "services/project.service"; -import { redirect } from "next/navigation"; - -type LayoutProps = { - params: { workspace_slug: string; project_slug: string }; -}; - -export async function generateMetadata({ params }: LayoutProps): Promise { - // read route params - const { workspace_slug, project_slug } = params; - const projectServiceInstance = new ProjectService(); - - try { - const project = await projectServiceInstance?.getProjectSettingsAsync(workspace_slug, project_slug); - - return { - title: `${project?.project_details?.name} | ${workspace_slug}`, - description: `${ - project?.project_details?.description || `${project?.project_details?.name} | ${workspace_slug}` - }`, - icons: `data:image/svg+xml,${ - typeof project?.project_details?.emoji != "object" - ? String.fromCodePoint(parseInt(project?.project_details?.emoji)) - : "✈️" - }`, - }; - } catch (error: any) { - if (error?.data?.error) { - redirect(`/project-not-published`); - } - return {}; - } -} - -const RootLayout = ({ children }: { children: React.ReactNode }) => ( -
-
- -
- {/*
- -
*/} -
{children}
- -
- -
- plane logo -
-
- Powered by Plane Deploy -
- -
-
-); - -export default RootLayout; diff --git a/apps/space/app/[workspace_slug]/[project_slug]/page.tsx b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx deleted file mode 100644 index 81c2b48c2..000000000 --- a/apps/space/app/[workspace_slug]/[project_slug]/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -// next imports -import { useRouter, useParams, useSearchParams } from "next/navigation"; -// mobx -import { observer } from "mobx-react-lite"; -// components -import { IssueListView } from "components/issues/board-views/list"; -import { IssueKanbanView } from "components/issues/board-views/kanban"; -import { IssueCalendarView } from "components/issues/board-views/calendar"; -import { IssueSpreadsheetView } from "components/issues/board-views/spreadsheet"; -import { IssueGanttView } from "components/issues/board-views/gantt"; -// mobx store -import { RootStore } from "store/root"; -import { useMobxStore } from "lib/mobx/store-provider"; -// types -import { TIssueBoardKeys } from "store/types"; - -const WorkspaceProjectPage = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const routerParams = useParams(); - const routerSearchparams = useSearchParams(); - - const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string }; - const board = - routerSearchparams && - routerSearchparams.get("board") != null && - (routerSearchparams.get("board") as TIssueBoardKeys | ""); - - // updating default board view when we are in the issues page - useEffect(() => { - if (workspace_slug && project_slug && store?.project?.workspaceProjectSettings) { - const workspacePRojectSettingViews = store?.project?.workspaceProjectSettings?.views; - const userAccessViews: TIssueBoardKeys[] = []; - - Object.keys(workspacePRojectSettingViews).filter((_key) => { - if (_key === "list" && workspacePRojectSettingViews.list === true) userAccessViews.push(_key); - if (_key === "kanban" && workspacePRojectSettingViews.kanban === true) userAccessViews.push(_key); - if (_key === "calendar" && workspacePRojectSettingViews.calendar === true) userAccessViews.push(_key); - if (_key === "spreadsheet" && workspacePRojectSettingViews.spreadsheet === true) userAccessViews.push(_key); - if (_key === "gantt" && workspacePRojectSettingViews.gantt === true) userAccessViews.push(_key); - }); - - if (userAccessViews && userAccessViews.length > 0) { - if (!board) { - store.issue.setCurrentIssueBoardView(userAccessViews[0]); - router.replace(`/${workspace_slug}/${project_slug}?board=${userAccessViews[0]}`); - } else { - if (userAccessViews.includes(board)) { - if (store.issue.currentIssueBoardView === null) store.issue.setCurrentIssueBoardView(board); - else { - if (board === store.issue.currentIssueBoardView) - router.replace(`/${workspace_slug}/${project_slug}?board=${board}`); - else { - store.issue.setCurrentIssueBoardView(board); - router.replace(`/${workspace_slug}/${project_slug}?board=${board}`); - } - } - } else { - store.issue.setCurrentIssueBoardView(userAccessViews[0]); - router.replace(`/${workspace_slug}/${project_slug}?board=${userAccessViews[0]}`); - } - } - } - } - }, [workspace_slug, project_slug, board, router, store?.issue, store?.project?.workspaceProjectSettings]); - - useEffect(() => { - if (workspace_slug && project_slug) { - store?.project?.getProjectSettingsAsync(workspace_slug, project_slug); - store?.issue?.getIssuesAsync(workspace_slug, project_slug); - } - }, [workspace_slug, project_slug, store?.project, store?.issue]); - - return ( -
- {store?.issue?.loader && !store.issue.issues ? ( -
Loading...
- ) : ( - <> - {store?.issue?.error ? ( -
Something went wrong.
- ) : ( - store?.issue?.currentIssueBoardView && ( - <> - {store?.issue?.currentIssueBoardView === "list" && ( -
-
- -
-
- )} - {store?.issue?.currentIssueBoardView === "kanban" && ( -
- -
- )} - {store?.issue?.currentIssueBoardView === "calendar" && } - {store?.issue?.currentIssueBoardView === "spreadsheet" && } - {store?.issue?.currentIssueBoardView === "gantt" && } - - ) - )} - - )} -
- ); -}); - -export default WorkspaceProjectPage; diff --git a/apps/space/app/layout.tsx b/apps/space/app/layout.tsx deleted file mode 100644 index b63f748e8..000000000 --- a/apps/space/app/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -// root styles -import "styles/globals.css"; -// mobx store provider -import { MobxStoreProvider } from "lib/mobx/store-provider"; -import MobxStoreInit from "lib/mobx/store-init"; - -const RootLayout = ({ children }: { children: React.ReactNode }) => ( - - - - -
{children}
-
- - -); - -export default RootLayout; diff --git a/apps/space/app/page.tsx b/apps/space/app/page.tsx deleted file mode 100644 index 6a18b7283..000000000 --- a/apps/space/app/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -import React from "react"; - -const HomePage = () => ( -
Plane Deploy
-); - -export default HomePage; diff --git a/apps/space/components/icons/index.ts b/apps/space/components/icons/index.ts deleted file mode 100644 index 5f23e0f3a..000000000 --- a/apps/space/components/icons/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./issue-group/backlog-state-icon"; -export * from "./issue-group/unstarted-state-icon"; -export * from "./issue-group/started-state-icon"; -export * from "./issue-group/completed-state-icon"; -export * from "./issue-group/cancelled-state-icon"; diff --git a/apps/space/components/issues/board-views/block-due-date.tsx b/apps/space/components/issues/board-views/block-due-date.tsx deleted file mode 100644 index 6d3cc3cc0..000000000 --- a/apps/space/components/issues/board-views/block-due-date.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -// helpers -import { renderDateFormat } from "constants/helpers"; - -export const findHowManyDaysLeft = (date: string | Date) => { - const today = new Date(); - const eventDate = new Date(date); - const timeDiff = Math.abs(eventDate.getTime() - today.getTime()); - return Math.ceil(timeDiff / (1000 * 3600 * 24)); -}; - -const validDate = (date: any, state: string): string => { - if (date === null || ["backlog", "unstarted", "cancelled"].includes(state)) - return `bg-gray-500/10 text-gray-500 border-gray-500/50`; - else { - const today = new Date(); - const dueDate = new Date(date); - - if (dueDate < today) return `bg-red-500/10 text-red-500 border-red-500/50`; - else return `bg-green-500/10 text-green-500 border-green-500/50`; - } -}; - -export const IssueBlockDueDate = ({ due_date, state }: any) => ( -
- {renderDateFormat(due_date)} -
-); diff --git a/apps/space/components/issues/board-views/block-labels.tsx b/apps/space/components/issues/board-views/block-labels.tsx deleted file mode 100644 index 90cc1629c..000000000 --- a/apps/space/components/issues/board-views/block-labels.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -export const IssueBlockLabels = ({ labels }: any) => ( -
- {labels && - labels.length > 0 && - labels.map((_label: any) => ( -
-
-
{_label?.name}
-
- ))} -
-); diff --git a/apps/space/components/issues/board-views/block-state.tsx b/apps/space/components/issues/board-views/block-state.tsx deleted file mode 100644 index 87cd65938..000000000 --- a/apps/space/components/issues/board-views/block-state.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -// constants -import { issueGroupFilter } from "constants/data"; - -export const IssueBlockState = ({ state }: any) => { - const stateGroup = issueGroupFilter(state.group); - - if (stateGroup === null) return <>; - return ( -
- -
{state?.name}
-
- ); -}; diff --git a/apps/space/components/issues/board-views/kanban/block.tsx b/apps/space/components/issues/board-views/kanban/block.tsx deleted file mode 100644 index 22af77568..000000000 --- a/apps/space/components/issues/board-views/kanban/block.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import { IssueBlockPriority } from "components/issues/board-views/block-priority"; -import { IssueBlockState } from "components/issues/board-views/block-state"; -import { IssueBlockLabels } from "components/issues/board-views/block-labels"; -import { IssueBlockDueDate } from "components/issues/board-views/block-due-date"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// interfaces -import { IIssue } from "store/types/issue"; -import { RootStore } from "store/root"; - -export const IssueListBlock = ({ issue }: { issue: IIssue }) => { - const store: RootStore = useMobxStore(); - - return ( -
- {/* id */} -
- {store?.project?.project?.identifier}-{issue?.sequence_id} -
- - {/* name */} -
{issue.name}
- - {/* priority */} -
- {issue?.priority && ( -
- -
- )} - {/* state */} - {issue?.state_detail && ( -
- -
- )} - {/* labels */} - {issue?.label_details && issue?.label_details.length > 0 && ( -
- -
- )} - {/* due date */} - {issue?.target_date && ( -
- -
- )} -
-
- ); -}; diff --git a/apps/space/components/issues/board-views/list/block.tsx b/apps/space/components/issues/board-views/list/block.tsx deleted file mode 100644 index b9dfcc6ab..000000000 --- a/apps/space/components/issues/board-views/list/block.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import { IssueBlockPriority } from "components/issues/board-views/block-priority"; -import { IssueBlockState } from "components/issues/board-views/block-state"; -import { IssueBlockLabels } from "components/issues/board-views/block-labels"; -import { IssueBlockDueDate } from "components/issues/board-views/block-due-date"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// interfaces -import { IIssue } from "store/types/issue"; -import { RootStore } from "store/root"; - -export const IssueListBlock = ({ issue }: { issue: IIssue }) => { - const store: RootStore = useMobxStore(); - - return ( -
-
- {/* id */} -
- {store?.project?.project?.identifier}-{issue?.sequence_id} -
- {/* name */} -
{issue.name}
-
- - {/* priority */} - {issue?.priority && ( -
- -
- )} - - {/* state */} - {issue?.state_detail && ( -
- -
- )} - - {/* labels */} - {issue?.label_details && issue?.label_details.length > 0 && ( -
- -
- )} - - {/* due date */} - {issue?.target_date && ( -
- -
- )} -
- ); -}; diff --git a/apps/space/components/issues/board-views/list/index.tsx b/apps/space/components/issues/board-views/list/index.tsx deleted file mode 100644 index 7a7ec0de1..000000000 --- a/apps/space/components/issues/board-views/list/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import { IssueListHeader } from "components/issues/board-views/list/header"; -import { IssueListBlock } from "components/issues/board-views/list/block"; -// interfaces -import { IIssueState, IIssue } from "store/types/issue"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -export const IssueListView = observer(() => { - const store: RootStore = useMobxStore(); - - return ( - <> - {store?.issue?.states && - store?.issue?.states.length > 0 && - store?.issue?.states.map((_state: IIssueState) => ( -
- - {store.issue.getFilteredIssuesByState(_state.id) && - store.issue.getFilteredIssuesByState(_state.id).length > 0 ? ( -
- {store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( - - ))} -
- ) : ( -
No Issues are available.
- )} -
- ))} - - ); -}); diff --git a/apps/space/components/issues/filters-render/date.tsx b/apps/space/components/issues/filters-render/date.tsx deleted file mode 100644 index e01d0ae58..000000000 --- a/apps/space/components/issues/filters-render/date.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; - -const IssueDateFilter = observer(() => { - const store = useMobxStore(); - - return ( - <> -
-
Due Date
-
- {/*
-
- close -
-
Backlog
-
- close -
-
*/} -
-
- close -
-
- - ); -}); - -export default IssueDateFilter; diff --git a/apps/space/components/issues/filters-render/index.tsx b/apps/space/components/issues/filters-render/index.tsx deleted file mode 100644 index 366ae1030..000000000 --- a/apps/space/components/issues/filters-render/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import IssueStateFilter from "./state"; -import IssueLabelFilter from "./label"; -import IssuePriorityFilter from "./priority"; -import IssueDateFilter from "./date"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const IssueFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const clearAllFilters = () => {}; - - return ( -
- {/* state */} - {store?.issue?.states && } - {/* labels */} - {store?.issue?.labels && } - {/* priority */} - - {/* due date */} - - {/* clear all filters */} -
-
Clear all filters
-
-
- ); -}); - -export default IssueFilter; diff --git a/apps/space/components/issues/filters-render/label/filter-label-block.tsx b/apps/space/components/issues/filters-render/label/filter-label-block.tsx deleted file mode 100644 index 0606bfc95..000000000 --- a/apps/space/components/issues/filters-render/label/filter-label-block.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// interfaces -import { IIssueLabel } from "store/types/issue"; -// constants -import { issueGroupFilter } from "constants/data"; - -export const RenderIssueLabel = observer(({ label }: { label: IIssueLabel }) => { - const store = useMobxStore(); - - const removeLabelFromFilter = () => {}; - - return ( -
-
-
-
-
{label?.name}
-
- close -
-
- ); -}); diff --git a/apps/space/components/issues/filters-render/label/index.tsx b/apps/space/components/issues/filters-render/label/index.tsx deleted file mode 100644 index 7d313153a..000000000 --- a/apps/space/components/issues/filters-render/label/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import { RenderIssueLabel } from "./filter-label-block"; -// interfaces -import { IIssueLabel } from "store/types/issue"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const IssueLabelFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const clearLabelFilters = () => {}; - - return ( - <> -
-
Labels
-
- {store?.issue?.labels && - store?.issue?.labels.map((_label: IIssueLabel, _index: number) => )} -
-
- close -
-
- - ); -}); - -export default IssueLabelFilter; diff --git a/apps/space/components/issues/filters-render/priority/filter-priority-block.tsx b/apps/space/components/issues/filters-render/priority/filter-priority-block.tsx deleted file mode 100644 index 98173fd66..000000000 --- a/apps/space/components/issues/filters-render/priority/filter-priority-block.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// interfaces -import { IIssuePriorityFilters } from "store/types/issue"; - -export const RenderIssuePriority = observer(({ priority }: { priority: IIssuePriorityFilters }) => { - const store = useMobxStore(); - - const removePriorityFromFilter = () => {}; - - return ( -
-
- {priority?.icon} -
-
{priority?.title}
-
- close -
-
- ); -}); diff --git a/apps/space/components/issues/filters-render/priority/index.tsx b/apps/space/components/issues/filters-render/priority/index.tsx deleted file mode 100644 index 2253a0be2..000000000 --- a/apps/space/components/issues/filters-render/priority/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// components -import { RenderIssuePriority } from "./filter-priority-block"; -// interfaces -import { IIssuePriorityFilters } from "store/types/issue"; -// constants -import { issuePriorityFilters } from "constants/data"; - -const IssuePriorityFilter = observer(() => { - const store = useMobxStore(); - - return ( - <> -
-
Priority
-
- {issuePriorityFilters.map((_priority: IIssuePriorityFilters, _index: number) => ( - - ))} -
-
- close -
-
{" "} - - ); -}); - -export default IssuePriorityFilter; diff --git a/apps/space/components/issues/filters-render/state/filter-state-block.tsx b/apps/space/components/issues/filters-render/state/filter-state-block.tsx deleted file mode 100644 index 95a4f4c70..000000000 --- a/apps/space/components/issues/filters-render/state/filter-state-block.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// interfaces -import { IIssueState } from "store/types/issue"; -// constants -import { issueGroupFilter } from "constants/data"; - -export const RenderIssueState = observer(({ state }: { state: IIssueState }) => { - const store = useMobxStore(); - - const stateGroup = issueGroupFilter(state.group); - - const removeStateFromFilter = () => {}; - - if (stateGroup === null) return <>; - return ( -
-
- -
-
{state?.name}
-
- close -
-
- ); -}); diff --git a/apps/space/components/issues/filters-render/state/index.tsx b/apps/space/components/issues/filters-render/state/index.tsx deleted file mode 100644 index fc73af381..000000000 --- a/apps/space/components/issues/filters-render/state/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import { RenderIssueState } from "./filter-state-block"; -// interfaces -import { IIssueState } from "store/types/issue"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const IssueStateFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const clearStateFilters = () => {}; - - return ( - <> -
-
State
-
- {store?.issue?.states && - store?.issue?.states.map((_state: IIssueState, _index: number) => )} -
-
- close -
-
- - ); -}); - -export default IssueStateFilter; diff --git a/apps/space/components/issues/navbar/index.tsx b/apps/space/components/issues/navbar/index.tsx deleted file mode 100644 index 71a2fabcc..000000000 --- a/apps/space/components/issues/navbar/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -// next imports -import Image from "next/image"; -// components -import { NavbarSearch } from "./search"; -import { NavbarIssueBoardView } from "./issue-board-view"; -import { NavbarIssueFilter } from "./issue-filter"; -import { NavbarIssueView } from "./issue-view"; -import { NavbarTheme } from "./theme"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const renderEmoji = (emoji: string | { name: string; color: string }) => { - if (!emoji) return; - - if (typeof emoji === "object") - return ( - - {emoji.name} - - ); - else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji)); -}; - -const IssueNavbar = observer(() => { - const store: RootStore = useMobxStore(); - - return ( -
- {/* project detail */} -
-
- {store?.project?.project && store?.project?.project?.emoji ? ( - renderEmoji(store?.project?.project?.emoji) - ) : ( - plane logo - )} -
-
- {store?.project?.project?.name || `...`} -
-
- - {/* issue search bar */} -
- -
- - {/* issue views */} -
- -
- - {/* issue filters */} - {/*
- - -
*/} - - {/* theming */} - {/*
- -
*/} -
- ); -}); - -export default IssueNavbar; diff --git a/apps/space/components/issues/navbar/issue-board-view.tsx b/apps/space/components/issues/navbar/issue-board-view.tsx deleted file mode 100644 index 57c8b27c1..000000000 --- a/apps/space/components/issues/navbar/issue-board-view.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -// next imports -import { useRouter, useParams } from "next/navigation"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// constants -import { issueViews } from "constants/data"; -// interfaces -import { TIssueBoardKeys } from "store/types"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -export const NavbarIssueBoardView = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const routerParams = useParams(); - - const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string }; - - const handleCurrentBoardView = (boardView: TIssueBoardKeys) => { - store?.issue?.setCurrentIssueBoardView(boardView); - router.replace(`/${workspace_slug}/${project_slug}?board=${boardView}`); - }; - - return ( - <> - {store?.project?.workspaceProjectSettings && - issueViews && - issueViews.length > 0 && - issueViews.map( - (_view) => - store?.project?.workspaceProjectSettings?.views[_view?.key] && ( -
handleCurrentBoardView(_view?.key)} - title={_view?.title} - > - - {_view?.icon} - -
- ) - )} - - ); -}); diff --git a/apps/space/components/issues/navbar/issue-filter.tsx b/apps/space/components/issues/navbar/issue-filter.tsx deleted file mode 100644 index 10255882d..000000000 --- a/apps/space/components/issues/navbar/issue-filter.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -export const NavbarIssueFilter = observer(() => { - const store: RootStore = useMobxStore(); - - return
Filter
; -}); diff --git a/apps/space/components/issues/navbar/theme.tsx b/apps/space/components/issues/navbar/theme.tsx deleted file mode 100644 index c122f8478..000000000 --- a/apps/space/components/issues/navbar/theme.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -export const NavbarTheme = observer(() => { - const store: RootStore = useMobxStore(); - - const handleTheme = () => { - store?.theme?.setTheme(store?.theme?.theme === "light" ? "dark" : "light"); - }; - - return ( -
- {store?.theme?.theme === "light" ? ( - dark_mode - ) : ( - light_mode - )} -
- ); -}); diff --git a/apps/space/constants/helpers.ts b/apps/space/constants/helpers.ts deleted file mode 100644 index fd4dba217..000000000 --- a/apps/space/constants/helpers.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const renderDateFormat = (date: string | Date | null) => { - if (!date) return "N/A"; - - var d = new Date(date), - month = "" + (d.getMonth() + 1), - day = "" + d.getDate(), - year = d.getFullYear(); - - if (month.length < 2) month = "0" + month; - if (day.length < 2) day = "0" + day; - - return [year, month, day].join("-"); -}; diff --git a/apps/space/lib/mobx/store-init.tsx b/apps/space/lib/mobx/store-init.tsx deleted file mode 100644 index a31bc822f..000000000 --- a/apps/space/lib/mobx/store-init.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const MobxStoreInit = () => { - const store: RootStore = useMobxStore(); - - useEffect(() => { - // theme - const _theme = localStorage && localStorage.getItem("app_theme") ? localStorage.getItem("app_theme") : "light"; - if (_theme && store?.theme?.theme != _theme) store.theme.setTheme(_theme); - else localStorage.setItem("app_theme", _theme && _theme != "light" ? "dark" : "light"); - }, [store?.theme]); - - return <>; -}; - -export default MobxStoreInit; diff --git a/apps/space/next.config.js b/apps/space/next.config.js deleted file mode 100644 index 4128b636a..000000000 --- a/apps/space/next.config.js +++ /dev/null @@ -1,14 +0,0 @@ -/** @type {import('next').NextConfig} */ -const path = require("path"); - -const nextConfig = { - reactStrictMode: false, - swcMinify: true, - experimental: { - outputFileTracingRoot: path.join(__dirname, "../../"), - appDir: true, - }, - output: "standalone", -}; - -module.exports = nextConfig; diff --git a/apps/space/package.json b/apps/space/package.json deleted file mode 100644 index e37dfa54a..000000000 --- a/apps/space/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "space", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "next dev -p 4000", - "build": "next build", - "start": "next start -p 4000", - "lint": "next lint" - }, - "dependencies": { - "@headlessui/react": "^1.7.13", - "@types/node": "18.14.1", - "@types/nprogress": "^0.2.0", - "@types/react": "18.0.28", - "@types/react-dom": "18.0.11", - "axios": "^1.3.4", - "eslint": "8.34.0", - "eslint-config-next": "13.2.1", - "js-cookie": "^3.0.1", - "mobx": "^6.10.0", - "mobx-react-lite": "^4.0.3", - "next": "^13.4.16", - "nprogress": "^0.2.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "4.9.5", - "uuid": "^9.0.0" - }, - "devDependencies": { - "@types/js-cookie": "^3.0.3", - "@types/uuid": "^9.0.1", - "autoprefixer": "^10.4.13", - "postcss": "^8.4.21", - "tailwindcss": "^3.2.7" - } -} diff --git a/apps/space/pages/_app.tsx b/apps/space/pages/_app.tsx deleted file mode 100644 index 8681006e1..000000000 --- a/apps/space/pages/_app.tsx +++ /dev/null @@ -1,10 +0,0 @@ -// styles -import "styles/globals.css"; -// types -import type { AppProps } from "next/app"; - -function MyApp({ Component, pageProps }: AppProps) { - return ; -} - -export default MyApp; diff --git a/apps/space/public/plane-logo.webp b/apps/space/public/plane-logo.webp deleted file mode 100644 index 52e7c98da..000000000 Binary files a/apps/space/public/plane-logo.webp and /dev/null differ diff --git a/apps/space/services/issue.service.ts b/apps/space/services/issue.service.ts deleted file mode 100644 index 38b2f7a1d..000000000 --- a/apps/space/services/issue.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -// services -import APIService from "services/api.service"; - -class IssueService extends APIService { - constructor() { - super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); - } - - async getPublicIssues(workspace_slug: string, project_slug: string): Promise { - return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/issues/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); - } -} - -export default IssueService; diff --git a/apps/space/store/issue.ts b/apps/space/store/issue.ts deleted file mode 100644 index 79ad4b910..000000000 --- a/apps/space/store/issue.ts +++ /dev/null @@ -1,91 +0,0 @@ -// mobx -import { observable, action, computed, makeObservable, runInAction } from "mobx"; -// service -import IssueService from "services/issue.service"; -// types -import { TIssueBoardKeys } from "store/types/issue"; -import { IIssueStore, IIssue, IIssueState, IIssueLabel } from "./types"; - -class IssueStore implements IIssueStore { - currentIssueBoardView: TIssueBoardKeys | null = null; - - loader: boolean = false; - error: any | null = null; - - states: IIssueState[] | null = null; - labels: IIssueLabel[] | null = null; - issues: IIssue[] | null = null; - - userSelectedStates: string[] = []; - userSelectedLabels: string[] = []; - // root store - rootStore; - // service - issueService; - - constructor(_rootStore: any) { - makeObservable(this, { - // observable - currentIssueBoardView: observable, - - loader: observable, - error: observable, - - states: observable.ref, - labels: observable.ref, - issues: observable.ref, - - userSelectedStates: observable, - userSelectedLabels: observable, - // action - setCurrentIssueBoardView: action, - getIssuesAsync: action, - // computed - }); - - this.rootStore = _rootStore; - this.issueService = new IssueService(); - } - - // computed - getCountOfIssuesByState(state_id: string): number { - return this.issues?.filter((issue) => issue.state == state_id).length || 0; - } - - getFilteredIssuesByState(state_id: string): IIssue[] | [] { - return this.issues?.filter((issue) => issue.state == state_id) || []; - } - - // action - setCurrentIssueBoardView = async (view: TIssueBoardKeys) => { - this.currentIssueBoardView = view; - }; - - getIssuesAsync = async (workspace_slug: string, project_slug: string) => { - try { - this.loader = true; - this.error = null; - - const response = await this.issueService.getPublicIssues(workspace_slug, project_slug); - - if (response) { - const _states: IIssueState[] = [...response?.states]; - const _labels: IIssueLabel[] = [...response?.labels]; - const _issues: IIssue[] = [...response?.issues]; - runInAction(() => { - this.states = _states; - this.labels = _labels; - this.issues = _issues; - this.loader = false; - }); - return response; - } - } catch (error) { - this.loader = false; - this.error = error; - return error; - } - }; -} - -export default IssueStore; diff --git a/apps/space/store/theme.ts b/apps/space/store/theme.ts deleted file mode 100644 index 809d56b97..000000000 --- a/apps/space/store/theme.ts +++ /dev/null @@ -1,33 +0,0 @@ -// mobx -import { observable, action, computed, makeObservable, runInAction } from "mobx"; -// types -import { IThemeStore } from "./types"; - -class ThemeStore implements IThemeStore { - theme: "light" | "dark" = "light"; - // root store - rootStore; - - constructor(_rootStore: any | null = null) { - makeObservable(this, { - // observable - theme: observable, - // action - setTheme: action, - // computed - }); - - this.rootStore = _rootStore; - } - - setTheme = async (_theme: "light" | "dark" | string) => { - try { - localStorage.setItem("app_theme", _theme); - this.theme = _theme === "light" ? "light" : "dark"; - } catch (error) { - console.error("setting user theme error", error); - } - }; -} - -export default ThemeStore; diff --git a/apps/space/store/types/index.ts b/apps/space/store/types/index.ts deleted file mode 100644 index 5a0a51eda..000000000 --- a/apps/space/store/types/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./user"; -export * from "./theme"; -export * from "./project"; -export * from "./issue"; diff --git a/apps/space/store/types/issue.ts b/apps/space/store/types/issue.ts deleted file mode 100644 index 5feeba7bd..000000000 --- a/apps/space/store/types/issue.ts +++ /dev/null @@ -1,72 +0,0 @@ -export type TIssueBoardKeys = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt"; - -export interface IIssueBoardViews { - key: TIssueBoardKeys; - title: string; - icon: string; - className: string; -} - -export type TIssuePriorityKey = "urgent" | "high" | "medium" | "low" | "none"; -export type TIssuePriorityTitle = "Urgent" | "High" | "Medium" | "Low" | "None"; -export interface IIssuePriorityFilters { - key: TIssuePriorityKey; - title: TIssuePriorityTitle; - className: string; - icon: string; -} - -export type TIssueGroupKey = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; -export type TIssueGroupTitle = "Backlog" | "Unstarted" | "Started" | "Completed" | "Cancelled"; - -export interface IIssueGroup { - key: TIssueGroupKey; - title: TIssueGroupTitle; - color: string; - className: string; - icon: React.FC; -} - -export interface IIssue { - id: string; - sequence_id: number; - name: string; - description_html: string; - priority: TIssuePriorityKey | null; - state: string; - state_detail: any; - label_details: any; - target_date: any; -} - -export interface IIssueState { - id: string; - name: string; - group: TIssueGroupKey; - color: string; -} - -export interface IIssueLabel { - id: string; - name: string; - color: string; -} - -export interface IIssueStore { - currentIssueBoardView: TIssueBoardKeys | null; - loader: boolean; - error: any | null; - - states: IIssueState[] | null; - labels: IIssueLabel[] | null; - issues: IIssue[] | null; - - userSelectedStates: string[]; - userSelectedLabels: string[]; - - getCountOfIssuesByState: (state: string) => number; - getFilteredIssuesByState: (state: string) => IIssue[]; - - setCurrentIssueBoardView: (view: TIssueBoardKeys) => void; - getIssuesAsync: (workspace_slug: string, project_slug: string) => Promise; -} diff --git a/apps/space/store/types/user.ts b/apps/space/store/types/user.ts deleted file mode 100644 index 0293c5381..000000000 --- a/apps/space/store/types/user.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IUserStore { - currentUser: any | null; - getUserAsync: () => void; -} diff --git a/apps/space/store/user.ts b/apps/space/store/user.ts deleted file mode 100644 index 2f4782236..000000000 --- a/apps/space/store/user.ts +++ /dev/null @@ -1,43 +0,0 @@ -// mobx -import { observable, action, computed, makeObservable, runInAction } from "mobx"; -// service -import UserService from "services/user.service"; -// types -import { IUserStore } from "./types"; - -class UserStore implements IUserStore { - currentUser: any | null = null; - // root store - rootStore; - // service - userService; - - constructor(_rootStore: any) { - makeObservable(this, { - // observable - currentUser: observable, - // actions - // computed - }); - this.rootStore = _rootStore; - this.userService = new UserService(); - } - - getUserAsync = async () => { - try { - const response = this.userService.currentUser(); - if (response) { - runInAction(() => { - this.currentUser = response; - }); - } - } catch (error) { - console.error("error", error); - runInAction(() => { - // render error actions - }); - } - }; -} - -export default UserStore; diff --git a/apps/space/styles/globals.css b/apps/space/styles/globals.css deleted file mode 100644 index e493f0abc..000000000 --- a/apps/space/styles/globals.css +++ /dev/null @@ -1,6 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0&display=swap"); - -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/apps/space/tailwind.config.js b/apps/space/tailwind.config.js deleted file mode 100644 index 55aaa9a31..000000000 --- a/apps/space/tailwind.config.js +++ /dev/null @@ -1,17 +0,0 @@ -/** @type {import('tailwindcss').Config} */ - -module.exports = { - content: [ - "./app/**/*.{js,ts,jsx,tsx}", - "./pages/**/*.{js,ts,jsx,tsx}", - "./layouts/**/*.tsx", - "./components/**/*.{js,ts,jsx,tsx}", - "./constants/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: { - colors: {}, - }, - }, - plugins: [], -}; diff --git a/apps/space/tsconfig.json b/apps/space/tsconfig.json deleted file mode 100644 index 5404bd9fb..000000000 --- a/apps/space/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "baseUrl": ".", - "paths": {}, - "plugins": [{ "name": "next" }] - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "server.js", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index fcb93c530..0e42c83a8 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -38,7 +38,7 @@ services: container_name: planefrontend image: makeplane/plane-frontend:latest restart: always - command: /usr/local/bin/start.sh apps/app/server.js app + command: /usr/local/bin/start.sh web/server.js web env_file: - .env environment: @@ -56,6 +56,20 @@ services: - plane-api - plane-worker + plane-deploy: + container_name: planedeploy + image: makeplane/plane-deploy:latest + restart: always + command: /usr/local/bin/start.sh space/server.js space + env_file: + - .env + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + depends_on: + - plane-api + - plane-worker + - plane-web + plane-api: container_name: planebackend image: makeplane/plane-backend:latest @@ -71,7 +85,7 @@ services: plane-worker: container_name: planebgworker - image: makeplane/plane-worker:latest + image: makeplane/plane-backend:latest restart: always command: ./bin/worker env_file: @@ -85,7 +99,7 @@ services: plane-beat-worker: container_name: planebeatworker - image: makeplane/plane-worker:latest + image: makeplane/plane-backend:latest restart: always command: ./bin/beat env_file: diff --git a/docker-compose.yml b/docker-compose.yml index f69f5be1d..cf631face 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,176 +1,202 @@ version: "3.8" -x-api-and-worker-env: - &api-and-worker-env - DEBUG: ${DEBUG} - SENTRY_DSN: ${SENTRY_DSN} - DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} - REDIS_URL: redis://plane-redis:6379/ - EMAIL_HOST: ${EMAIL_HOST} - EMAIL_HOST_USER: ${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} - EMAIL_PORT: ${EMAIL_PORT} - EMAIL_FROM: ${EMAIL_FROM} - EMAIL_USE_TLS: ${EMAIL_USE_TLS} - EMAIL_USE_SSL: ${EMAIL_USE_SSL} - AWS_REGION: ${AWS_REGION} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} - FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} - WEB_URL: ${WEB_URL} - GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} - DISABLE_COLLECTSTATIC: 1 - DOCKERIZED: 1 - OPENAI_API_BASE: ${OPENAI_API_BASE} - OPENAI_API_KEY: ${OPENAI_API_KEY} - GPT_ENGINE: ${GPT_ENGINE} - SECRET_KEY: ${SECRET_KEY} - DEFAULT_EMAIL: ${DEFAULT_EMAIL} - DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} - USE_MINIO: ${USE_MINIO} - ENABLE_SIGNUP: ${ENABLE_SIGNUP} +x-api-and-worker-env: &api-and-worker-env + DEBUG: ${DEBUG} + SENTRY_DSN: ${SENTRY_DSN} + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} + REDIS_URL: redis://plane-redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + EMAIL_PORT: ${EMAIL_PORT} + EMAIL_FROM: ${EMAIL_FROM} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} + EMAIL_USE_SSL: ${EMAIL_USE_SSL} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} + WEB_URL: ${WEB_URL} + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_BASE: ${OPENAI_API_BASE} + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} + DEFAULT_EMAIL: ${DEFAULT_EMAIL} + DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} + USE_MINIO: ${USE_MINIO} + ENABLE_SIGNUP: ${ENABLE_SIGNUP} services: - plane-web: - container_name: planefrontend - build: - context: . - dockerfile: ./apps/app/Dockerfile.web - args: - NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 - NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces - restart: always - command: /usr/local/bin/start.sh apps/app/server.js app - env_file: - - .env - environment: - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL} - NEXT_PUBLIC_GOOGLE_CLIENTID: "0" - NEXT_PUBLIC_GITHUB_APP_NAME: "0" - NEXT_PUBLIC_GITHUB_ID: "0" - NEXT_PUBLIC_SENTRY_DSN: "0" - NEXT_PUBLIC_ENABLE_OAUTH: "0" - NEXT_PUBLIC_ENABLE_SENTRY: "0" - NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0" - NEXT_PUBLIC_TRACK_EVENTS: "0" - depends_on: - - plane-api - - plane-worker + plane-web: + container_name: planefrontend + build: + context: . + dockerfile: ./web/Dockerfile.web + args: + DOCKER_BUILDKIT: 1 + NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 + NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces + restart: always + command: /usr/local/bin/start.sh web/server.js web + env_file: + - .env + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL} + NEXT_PUBLIC_GOOGLE_CLIENTID: "0" + NEXT_PUBLIC_GITHUB_APP_NAME: "0" + NEXT_PUBLIC_GITHUB_ID: "0" + NEXT_PUBLIC_SENTRY_DSN: "0" + NEXT_PUBLIC_ENABLE_OAUTH: "0" + NEXT_PUBLIC_ENABLE_SENTRY: "0" + NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0" + NEXT_PUBLIC_TRACK_EVENTS: "0" + depends_on: + - plane-api + - plane-worker - plane-api: - container_name: planebackend - build: - context: ./apiserver - dockerfile: Dockerfile.api - restart: always - command: ./bin/takeoff - env_file: - - .env - environment: - <<: *api-and-worker-env - depends_on: - - plane-db - - plane-redis + plane-deploy: + container_name: planedeploy + build: + context: . + dockerfile: ./space/Dockerfile.space + args: + DOCKER_BUILDKIT: 1 + NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 + NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 + restart: always + command: /usr/local/bin/start.sh space/server.js space + env_file: + - .env + environment: + - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} + depends_on: + - plane-api + - plane-worker + - plane-web - plane-worker: - container_name: planebgworker - build: - context: ./apiserver - dockerfile: Dockerfile.api - restart: always - command: ./bin/worker - env_file: - - .env - environment: - <<: *api-and-worker-env - depends_on: - - plane-api - - plane-db - - plane-redis + plane-api: + container_name: planebackend + build: + context: ./apiserver + dockerfile: Dockerfile.api + args: + DOCKER_BUILDKIT: 1 + restart: always + command: ./bin/takeoff + env_file: + - .env + environment: + <<: *api-and-worker-env + depends_on: + - plane-db + - plane-redis - plane-beat-worker: - container_name: planebeatworker - build: - context: ./apiserver - dockerfile: Dockerfile.api - restart: always - command: ./bin/beat - env_file: - - .env - environment: - <<: *api-and-worker-env - depends_on: - - plane-api - - plane-db - - plane-redis + plane-worker: + container_name: planebgworker + build: + context: ./apiserver + dockerfile: Dockerfile.api + args: + DOCKER_BUILDKIT: 1 + restart: always + command: ./bin/worker + env_file: + - .env + environment: + <<: *api-and-worker-env + depends_on: + - plane-api + - plane-db + - plane-redis - plane-db: - container_name: plane-db - image: postgres:15.2-alpine - restart: always - command: postgres -c 'max_connections=1000' - volumes: - - pgdata:/var/lib/postgresql/data - env_file: - - .env - environment: - POSTGRES_USER: ${PGUSER} - POSTGRES_DB: ${PGDATABASE} - POSTGRES_PASSWORD: ${PGPASSWORD} - PGDATA: /var/lib/postgresql/data + plane-beat-worker: + container_name: planebeatworker + build: + context: ./apiserver + dockerfile: Dockerfile.api + args: + DOCKER_BUILDKIT: 1 + restart: always + command: ./bin/beat + env_file: + - .env + environment: + <<: *api-and-worker-env + depends_on: + - plane-api + - plane-db + - plane-redis - plane-redis: - container_name: plane-redis - image: redis:6.2.7-alpine - restart: always - volumes: - - redisdata:/data + plane-db: + container_name: plane-db + image: postgres:15.2-alpine + restart: always + command: postgres -c 'max_connections=1000' + volumes: + - pgdata:/var/lib/postgresql/data + env_file: + - .env + environment: + POSTGRES_USER: ${PGUSER} + POSTGRES_DB: ${PGDATABASE} + POSTGRES_PASSWORD: ${PGPASSWORD} + PGDATA: /var/lib/postgresql/data - plane-minio: - container_name: plane-minio - image: minio/minio - restart: always - command: server /export --console-address ":9090" - volumes: - - uploads:/export - env_file: - - .env - environment: - MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} - MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} + plane-redis: + container_name: plane-redis + image: redis:6.2.7-alpine + restart: always + volumes: + - redisdata:/data - createbuckets: - image: minio/mc - entrypoint: > - /bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; " - env_file: - - .env - depends_on: - - plane-minio + plane-minio: + container_name: plane-minio + image: minio/minio + restart: always + command: server /export --console-address ":9090" + volumes: + - uploads:/export + env_file: + - .env + environment: + MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} + MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} - # Comment this if you already have a reverse proxy running - plane-proxy: - container_name: planeproxy - build: - context: ./nginx - dockerfile: Dockerfile - restart: always - ports: - - ${NGINX_PORT}:80 - env_file: - - .env - environment: - FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} - BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} - depends_on: - - plane-web - - plane-api + createbuckets: + image: minio/mc + entrypoint: > + /bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; " + env_file: + - .env + depends_on: + - plane-minio + + # Comment this if you already have a reverse proxy running + plane-proxy: + container_name: planeproxy + build: + context: ./nginx + dockerfile: Dockerfile + restart: always + ports: + - ${NGINX_PORT}:80 + env_file: + - .env + environment: + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} + BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} + depends_on: + - plane-web + - plane-api volumes: - pgdata: - redisdata: - uploads: + pgdata: + redisdata: + uploads: diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index 206c94b51..974f4907d 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -19,6 +19,10 @@ server { proxy_pass http://planebackend:8000/api/; } + location /spaces/ { + proxy_pass http://planedeploy:3000/spaces/; + } + location /${BUCKET_NAME}/ { proxy_pass http://plane-minio:9000/uploads/; } diff --git a/package.json b/package.json index 804fb7b64..793a1922f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "license": "AGPL-3.0", "private": true, "workspaces": [ - "apps/*", + "web", + "space", "packages/*" ], "scripts": { @@ -11,7 +12,8 @@ "dev": "turbo run dev", "start": "turbo run start", "lint": "turbo run lint", - "clean": "turbo run clean" + "clean": "turbo run clean", + "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "devDependencies": { "eslint-config-custom": "*", diff --git a/packages/eslint-config-custom/index.js b/packages/eslint-config-custom/index.js index 4d8295125..d31a76406 100644 --- a/packages/eslint-config-custom/index.js +++ b/packages/eslint-config-custom/index.js @@ -4,7 +4,7 @@ module.exports = { plugins: ["react", "@typescript-eslint"], settings: { next: { - rootDir: ["app/", "docs/", "packages/*/"], + rootDir: ["web/", "space/", "packages/*/"], }, }, rules: { diff --git a/space/.env.example b/space/.env.example new file mode 100644 index 000000000..238f70854 --- /dev/null +++ b/space/.env.example @@ -0,0 +1,8 @@ +# Base url for the API requests +NEXT_PUBLIC_API_BASE_URL="" +# Public boards deploy URL +NEXT_PUBLIC_DEPLOY_URL="" +# Google Client ID for Google OAuth +NEXT_PUBLIC_GOOGLE_CLIENTID="" +# Flag to toggle OAuth +NEXT_PUBLIC_ENABLE_OAUTH=1 \ No newline at end of file diff --git a/apps/app/.eslintrc.js b/space/.eslintrc.js similarity index 100% rename from apps/app/.eslintrc.js rename to space/.eslintrc.js diff --git a/apps/space/.gitignore b/space/.gitignore similarity index 100% rename from apps/space/.gitignore rename to space/.gitignore diff --git a/apps/space/.prettierignore b/space/.prettierignore similarity index 100% rename from apps/space/.prettierignore rename to space/.prettierignore diff --git a/apps/space/.prettierrc.json b/space/.prettierrc.json similarity index 100% rename from apps/space/.prettierrc.json rename to space/.prettierrc.json diff --git a/apps/app/Dockerfile.web b/space/Dockerfile.space similarity index 53% rename from apps/app/Dockerfile.web rename to space/Dockerfile.space index 2b28e1fd1..963dad136 100644 --- a/apps/app/Dockerfile.web +++ b/space/Dockerfile.space @@ -1,61 +1,56 @@ FROM node:18-alpine AS builder RUN apk add --no-cache libc6-compat -# Set working directory WORKDIR /app ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER RUN yarn global add turbo COPY . . -RUN turbo prune --scope=app --docker +RUN turbo prune --scope=space --docker -# Add lockfile and package.json's of isolated subworkspace FROM node:18-alpine AS installer RUN apk add --no-cache libc6-compat WORKDIR /app -ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 -# First install the dependencies (as they change less often) COPY .gitignore .gitignore COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/yarn.lock ./yarn.lock RUN yarn install --network-timeout 500000 -# Build the project COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json COPY replace-env-vars.sh /usr/local/bin/ USER root RUN chmod +x /usr/local/bin/replace-env-vars.sh -RUN yarn turbo run build --filter=app +ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1 -ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ - BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL -ENV NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL} -RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} app +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX + +RUN yarn turbo run build --filter=space + +RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} space FROM node:18-alpine AS runner WORKDIR /app -# Don't run production as root RUN addgroup --system --gid 1001 plane RUN adduser --system --uid 1001 captain USER captain -COPY --from=installer /app/apps/app/next.config.js . -COPY --from=installer /app/apps/app/package.json . +COPY --from=installer /app/space/next.config.js . +COPY --from=installer /app/space/package.json . -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./ +COPY --from=installer --chown=captain:plane /app/space/.next/standalone ./ -COPY --from=installer --chown=captain:plane /app/apps/app/.next ./apps/app/.next +COPY --from=installer --chown=captain:plane /app/space/.next ./space/.next +COPY --from=installer --chown=captain:plane /app/space/public ./space/public ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 -ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \ - BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL +ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1 +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX USER root COPY replace-env-vars.sh /usr/local/bin/ diff --git a/apps/space/README.md b/space/README.md similarity index 100% rename from apps/space/README.md rename to space/README.md diff --git a/space/components/accounts/email-code-form.tsx b/space/components/accounts/email-code-form.tsx new file mode 100644 index 000000000..b760ccfbb --- /dev/null +++ b/space/components/accounts/email-code-form.tsx @@ -0,0 +1,216 @@ +import React, { useEffect, useState, useCallback } from "react"; + +// react hook form +import { useForm } from "react-hook-form"; + +// services +import authenticationService from "services/authentication.service"; + +// hooks +import useToast from "hooks/use-toast"; +import useTimer from "hooks/use-timer"; + +// ui +import { Input, PrimaryButton } from "components/ui"; + +// types +type EmailCodeFormValues = { + email: string; + key?: string; + token?: string; +}; + +export const EmailCodeForm = ({ handleSignIn }: any) => { + const [codeSent, setCodeSent] = useState(false); + const [codeResent, setCodeResent] = useState(false); + const [isCodeResending, setIsCodeResending] = useState(false); + const [errorResendingCode, setErrorResendingCode] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const { setToastAlert } = useToast(); + const { timer: resendCodeTimer, setTimer: setResendCodeTimer } = useTimer(); + + const { + register, + handleSubmit, + setError, + setValue, + getValues, + watch, + formState: { errors, isSubmitting, isValid, isDirty }, + } = useForm({ + defaultValues: { + email: "", + key: "", + token: "", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting || errorResendingCode; + + const onSubmit = useCallback( + async ({ email }: EmailCodeFormValues) => { + setErrorResendingCode(false); + await authenticationService + .emailCode({ email }) + .then((res) => { + setValue("key", res.key); + setCodeSent(true); + }) + .catch((err) => { + setErrorResendingCode(true); + setToastAlert({ + title: "Oops!", + type: "error", + message: err?.error, + }); + }); + }, + [setToastAlert, setValue] + ); + + const handleSignin = async (formData: EmailCodeFormValues) => { + setIsLoading(true); + await authenticationService + .magicSignIn(formData) + .then((response) => { + setIsLoading(false); + handleSignIn(response); + }) + .catch((error) => { + setIsLoading(false); + setToastAlert({ + title: "Oops!", + type: "error", + message: error?.response?.data?.error ?? "Enter the correct code to sign in", + }); + setError("token" as keyof EmailCodeFormValues, { + type: "manual", + message: error?.error, + }); + }); + }; + + const emailOld = getValues("email"); + + useEffect(() => { + setErrorResendingCode(false); + }, [emailOld]); + + useEffect(() => { + const submitForm = (e: KeyboardEvent) => { + if (!codeSent && e.key === "Enter") { + e.preventDefault(); + handleSubmit(onSubmit)().then(() => { + setResendCodeTimer(30); + }); + } + }; + + if (!codeSent) { + window.addEventListener("keydown", submitForm); + } + + return () => { + window.removeEventListener("keydown", submitForm); + }; + }, [handleSubmit, codeSent, onSubmit, setResendCodeTimer]); + + return ( + <> + {(codeSent || codeResent) && ( +

+ We have sent the sign in code. +
+ Please check your inbox at {watch("email")} +

+ )} +
+
+ + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email address is not valid", + })} + /> + {errors.email &&
{errors.email.message}
} +
+ + {codeSent && ( + <> + + {errors.token &&
{errors.token.message}
} + + + )} + {codeSent ? ( + + {isLoading ? "Signing in..." : "Sign in"} + + ) : ( + { + handleSubmit(onSubmit)().then(() => { + setResendCodeTimer(30); + }); + }} + disabled={!isValid && isDirty} + loading={isSubmitting} + > + {isSubmitting ? "Sending code..." : "Send sign in code"} + + )} +
+ + ); +}; diff --git a/space/components/accounts/email-password-form.tsx b/space/components/accounts/email-password-form.tsx new file mode 100644 index 000000000..23742eefe --- /dev/null +++ b/space/components/accounts/email-password-form.tsx @@ -0,0 +1,116 @@ +import React, { useState } from "react"; + +import { useRouter } from "next/router"; +import Link from "next/link"; + +// react hook form +import { useForm } from "react-hook-form"; +// components +import { EmailResetPasswordForm } from "./email-reset-password-form"; +// ui +import { Input, PrimaryButton } from "components/ui"; +// types +type EmailPasswordFormValues = { + email: string; + password?: string; + medium?: string; +}; + +type Props = { + onSubmit: (formData: EmailPasswordFormValues) => Promise; +}; + +export const EmailPasswordForm: React.FC = ({ onSubmit }) => { + const [isResettingPassword, setIsResettingPassword] = useState(false); + + const router = useRouter(); + const isSignUpPage = router.pathname === "/sign-up"; + + const { + register, + handleSubmit, + formState: { errors, isSubmitting, isValid, isDirty }, + } = useForm({ + defaultValues: { + email: "", + password: "", + medium: "email", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + return ( + <> +

+ {isResettingPassword ? "Reset your password" : isSignUpPage ? "Sign up on Plane" : "Sign in to Plane"} +

+ {isResettingPassword ? ( + + ) : ( +
+
+ + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email address is not valid", + })} + placeholder="Enter your email address..." + className="border-custom-border-300 h-[46px]" + /> + {errors.email &&
{errors.email.message}
} +
+
+ + {errors.password &&
{errors.password.message}
} +
+
+ {isSignUpPage ? ( + + Already have an account? Sign in. + + ) : ( + + )} +
+
+ + {isSignUpPage ? (isSubmitting ? "Signing up..." : "Sign up") : isSubmitting ? "Signing in..." : "Sign in"} + + {!isSignUpPage && ( + + + Don{"'"}t have an account? Sign up. + + + )} +
+
+ )} + + ); +}; diff --git a/space/components/accounts/email-reset-password-form.tsx b/space/components/accounts/email-reset-password-form.tsx new file mode 100644 index 000000000..c850b305c --- /dev/null +++ b/space/components/accounts/email-reset-password-form.tsx @@ -0,0 +1,89 @@ +import React from "react"; + +// react hook form +import { useForm } from "react-hook-form"; +// services +import userService from "services/user.service"; +// hooks +// import useToast from "hooks/use-toast"; +// ui +import { Input, PrimaryButton, SecondaryButton } from "components/ui"; +// types +type Props = { + setIsResettingPassword: React.Dispatch>; +}; + +export const EmailResetPasswordForm: React.FC = ({ setIsResettingPassword }) => { + // const { setToastAlert } = useToast(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + email: "", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const forgotPassword = async (formData: any) => { + const payload = { + email: formData.email, + }; + + // await userService + // .forgotPassword(payload) + // .then(() => + // setToastAlert({ + // type: "success", + // title: "Success!", + // message: "Password reset link has been sent to your email address.", + // }) + // ) + // .catch((err) => { + // if (err.status === 400) + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: "Please check the Email ID entered.", + // }); + // else + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: "Something went wrong. Please try again.", + // }); + // }); + }; + + return ( +
+
+ + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email address is not valid", + })} + placeholder="Enter registered email address.." + className="border-custom-border-300 h-[46px]" + /> + {errors.email &&
{errors.email.message}
} +
+
+ setIsResettingPassword(false)}> + Go Back + + + {isSubmitting ? "Sending link..." : "Send reset link"} + +
+
+ ); +}; diff --git a/space/components/accounts/github-login-button.tsx b/space/components/accounts/github-login-button.tsx new file mode 100644 index 000000000..e9b30ab73 --- /dev/null +++ b/space/components/accounts/github-login-button.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState, FC } from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { useRouter } from "next/router"; +// next-themes +import { useTheme } from "next-themes"; +// images +import githubBlackImage from "public/logos/github-black.svg"; +import githubWhiteImage from "public/logos/github-white.svg"; + +export interface GithubLoginButtonProps { + handleSignIn: React.Dispatch; +} + +export const GithubLoginButton: FC = ({ handleSignIn }) => { + const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); + const [gitCode, setGitCode] = useState(null); + + const router = useRouter(); + + const { code } = router.query; + + const { theme } = useTheme(); + + useEffect(() => { + if (code && !gitCode) { + setGitCode(code.toString()); + handleSignIn(code.toString()); + } + }, [code, gitCode, handleSignIn]); + + useEffect(() => { + const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + setLoginCallBackURL(`${origin}/` as any); + }, []); + + return ( +
+ + + +
+ ); +}; diff --git a/space/components/accounts/google-login.tsx b/space/components/accounts/google-login.tsx new file mode 100644 index 000000000..82916d7b5 --- /dev/null +++ b/space/components/accounts/google-login.tsx @@ -0,0 +1,59 @@ +import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react"; + +import Script from "next/script"; + +export interface IGoogleLoginButton { + text?: string; + handleSignIn: React.Dispatch; + styles?: CSSProperties; +} + +export const GoogleLoginButton: FC = ({ handleSignIn }) => { + const googleSignInButton = useRef(null); + const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false); + + const loadScript = useCallback(() => { + if (!googleSignInButton.current || gsiScriptLoaded) return; + + (window as any)?.google?.accounts.id.initialize({ + client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "", + callback: handleSignIn, + }); + + try { + (window as any)?.google?.accounts.id.renderButton( + googleSignInButton.current, + { + type: "standard", + theme: "outline", + size: "large", + logo_alignment: "center", + width: 360, + text: "signin_with", + } as any // customization attributes + ); + } catch (err) { + console.log(err); + } + + (window as any)?.google?.accounts.id.prompt(); // also display the One Tap dialog + + setGsiScriptLoaded(true); + }, [handleSignIn, gsiScriptLoaded]); + + useEffect(() => { + if ((window as any)?.google?.accounts?.id) { + loadScript(); + } + return () => { + (window as any)?.google?.accounts.id.cancel(); + }; + }, [loadScript]); + + return ( + <> +