diff --git a/.eslintrc.js b/.eslintrc.js index 463c86901c0..c229c095269 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 new file mode 100644 index 00000000000..438bdbef39d --- /dev/null +++ b/.github/workflows/Build_Test_Pull_Request.yml @@ -0,0 +1,55 @@ +name: Build Pull Request Contents + +on: + pull_request: + types: ["opened", "synchronize"] + +jobs: + build-pull-request-contents: + name: Build Pull Request Contents + runs-on: ubuntu-20.04 + permissions: + pull-requests: read + + steps: + - name: Checkout Repository to Actions + uses: actions/checkout@v3.3.0 + + - name: Setup Node.js 18.x + uses: actions/setup-node@v2 + with: + node-version: 18.x + cache: 'yarn' + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v38 + with: + files_yaml: | + apiserver: + - apiserver/** + web: + - web/** + deploy: + - space/** + + - name: Setup .npmrc for repository + run: | + echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc + + - name: Build Plane's Main App + if: steps.changed-files.outputs.web_any_changed == 'true' + run: | + 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 space + yarn + yarn build + + diff --git a/.github/workflows/Update_Docker_Images.yml b/.github/workflows/Update_Docker_Images.yml new file mode 100644 index 00000000000..57dbb4a6717 --- /dev/null +++ b/.github/workflows/Update_Docker_Images.yml @@ -0,0 +1,111 @@ +name: Update Docker Images for Plane on Release + +on: + release: + types: [released] + +jobs: + build_push_backend: + name: Build and Push Api Server Docker Image + runs-on: ubuntu-20.04 + + steps: + - name: Check out the repo + uses: actions/checkout@v3.3.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Setup .npmrc for repository + run: | + echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc + + - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release + id: metaFrontend + uses: docker/metadata-action@v4.3.0 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend + tags: | + type=ref,event=tag + + - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release + id: metaBackend + uses: docker/metadata-action@v4.3.0 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend + tags: | + type=ref,event=tag + + - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release + id: metaDeploy + uses: docker/metadata-action@v4.3.0 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy + tags: | + type=ref,event=tag + + - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release + id: metaProxy + uses: docker/metadata-action@v4.3.0 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy + tags: | + type=ref,event=tag + + - name: Build and Push Frontend to Docker Container Registry + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./web/Dockerfile.web + platforms: linux/amd64 + tags: ${{ steps.metaFrontend.outputs.tags }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and Push Backend to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: ./apiserver + file: ./apiserver/Dockerfile.api + platforms: linux/amd64 + push: true + tags: ${{ steps.metaBackend.outputs.tags }} + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and Push Plane-Deploy to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./space/Dockerfile.space + platforms: linux/amd64 + push: true + tags: ${{ steps.metaDeploy.outputs.tags }} + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and Push Plane-Proxy to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: ./nginx + file: ./nginx/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.metaProxy.outputs.tags }} + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/push-image-backend.yml b/.github/workflows/push-image-backend.yml deleted file mode 100644 index 95d93f8133e..00000000000 --- a/.github/workflows/push-image-backend.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Build and Push Backend Docker Image - -on: - push: - branches: - - 'develop' - - 'master' - tags: - - '*' - -jobs: - build_push_backend: - name: Build and Push Api Server Docker Image - runs-on: ubuntu-20.04 - - steps: - - name: Check out the repo - uses: actions/checkout@v3.3.0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2.1.0 - with: - platforms: linux/arm64,linux/amd64 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2.5.0 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2.1.0 - with: - registry: "ghcr.io" - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to Docker Hub - uses: docker/login-action@v2.1.0 - with: - registry: "registry.hub.docker.com" - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker (Docker Hub) - id: ghmeta - uses: docker/metadata-action@v4.3.0 - with: - images: makeplane/plane-backend - - - name: Extract metadata (tags, labels) for Docker (Github) - id: dkrmeta - uses: docker/metadata-action@v4.3.0 - with: - images: ghcr.io/${{ github.repository }}-backend - - - name: Build and Push to GitHub Container Registry - uses: docker/build-push-action@v4.0.0 - with: - context: ./apiserver - file: ./apiserver/Dockerfile.api - platforms: linux/arm64,linux/amd64 - push: true - cache-from: type=gha - cache-to: type=gha - tags: ${{ steps.ghmeta.outputs.tags }} - labels: ${{ steps.ghmeta.outputs.labels }} - - - name: Build and Push to Docker Hub - uses: docker/build-push-action@v4.0.0 - with: - context: ./apiserver - file: ./apiserver/Dockerfile.api - platforms: linux/arm64,linux/amd64 - push: true - cache-from: type=gha - cache-to: type=gha - tags: ${{ steps.dkrmeta.outputs.tags }} - labels: ${{ steps.dkrmeta.outputs.labels }} - diff --git a/.github/workflows/push-image-frontend.yml b/.github/workflows/push-image-frontend.yml deleted file mode 100644 index cbd7425116d..00000000000 --- a/.github/workflows/push-image-frontend.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Build and Push Frontend Docker Image - -on: - push: - branches: - - 'develop' - - 'master' - tags: - - '*' - -jobs: - build_push_frontend: - name: Build Frontend Docker Image - runs-on: ubuntu-20.04 - - steps: - - name: Check out the repo - uses: actions/checkout@v3.3.0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2.1.0 - with: - platforms: linux/arm64,linux/amd64 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2.5.0 - - - name: Login to Github Container Registry - uses: docker/login-action@v2.1.0 - with: - registry: "ghcr.io" - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to Docker Hub - uses: docker/login-action@v2.1.0 - with: - registry: "registry.hub.docker.com" - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker (Docker Hub) - id: ghmeta - uses: docker/metadata-action@v4.3.0 - with: - images: makeplane/plane-frontend - - - name: Extract metadata (tags, labels) for Docker (Github) - id: meta - uses: docker/metadata-action@v4.3.0 - with: - images: ghcr.io/${{ github.repository }}-frontend - - - name: Build and Push to GitHub Container Registry - uses: docker/build-push-action@v4.0.0 - with: - context: . - file: ./apps/app/Dockerfile.web - platforms: linux/arm64,linux/amd64 - push: true - cache-from: type=gha - cache-to: type=gha - tags: ${{ steps.ghmeta.outputs.tags }} - labels: ${{ steps.ghmeta.outputs.labels }} - - - name: Build and Push to Docker Container Registry - uses: docker/build-push-action@v4.0.0 - with: - context: . - file: ./apps/app/Dockerfile.web - platforms: linux/arm64,linux/amd64 - push: true - cache-from: type=gha - cache-to: type=gha - tags: ${{ steps.dkrmeta.outputs.tags }} - labels: ${{ steps.dkrmeta.outputs.labels }} - diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 294dc1c0eb9..cd74b612133 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 2bc2764f3bc..a5a7ddd87d8 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/Procfile b/apiserver/Procfile index 694c49df409..63736e8e8a8 100644 --- a/apiserver/Procfile +++ b/apiserver/Procfile @@ -1,3 +1,3 @@ -web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile - +web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile - worker: celery -A plane worker -l info beat: celery -A plane beat -l INFO \ No newline at end of file diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 5855f0413b6..2dc910cafcd 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -20,6 +20,7 @@ ProjectMemberLiteSerializer, ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, + ProjectPublicMemberSerializer ) from .state import StateSerializer, StateLiteSerializer from .view import IssueViewSerializer, IssueViewFavoriteSerializer @@ -44,6 +45,7 @@ IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssuePublicSerializer, ) from .module import ( diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 64ee2b8f75d..938c7cab495 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -113,7 +113,11 @@ class Meta: ] def validate(self, data): - if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None): + if ( + data.get("start_date", None) is not None + and data.get("target_date", None) is not None + and data.get("start_date", None) > data.get("target_date", None) + ): raise serializers.ValidationError("Start date cannot exceed target date") return data @@ -510,6 +514,9 @@ class Meta: class IssueReactionSerializer(BaseSerializer): + + actor_detail = UserLiteSerializer(read_only=True, source="actor") + class Meta: model = IssueReaction fields = "__all__" @@ -521,19 +528,6 @@ class Meta: ] -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") @@ -554,12 +548,13 @@ class Meta: read_only_fields = ["workspace", "project", "comment", "actor"] - 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 @@ -569,7 +564,7 @@ class IssueCommentSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True) - + is_member = serializers.BooleanField(read_only=True) class Meta: model = IssueComment @@ -582,7 +577,6 @@ class Meta: "updated_by", "created_at", "updated_at", - "access", ] @@ -632,7 +626,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 @@ -658,7 +652,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 @@ -676,6 +670,33 @@ class Meta: ] +class IssuePublicSerializer(BaseSerializer): + project_detail = ProjectLiteSerializer(read_only=True, source="project") + state_detail = StateLiteSerializer(read_only=True, source="state") + reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions") + votes = IssueVoteSerializer(read_only=True, many=True) + + class Meta: + model = Issue + fields = [ + "id", + "name", + "description_html", + "sequence_id", + "state", + "state_detail", + "project", + "project_detail", + "workspace", + "priority", + "target_date", + "reactions", + "votes", + ] + read_only_fields = fields + + + class IssueSubscriberSerializer(BaseSerializer): class Meta: model = IssueSubscriber diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 1f7a973c1d8..49d986cae0b 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -15,6 +15,7 @@ ProjectIdentifier, ProjectFavorite, ProjectDeployBoard, + ProjectPublicMember, ) @@ -112,7 +113,7 @@ class Meta: class ProjectMemberSerializer(BaseSerializer): - workspace = WorkSpaceSerializer(read_only=True) + workspace = WorkspaceLiteSerializer(read_only=True) project = ProjectLiteSerializer(read_only=True) member = UserLiteSerializer(read_only=True) @@ -177,5 +178,17 @@ class Meta: fields = "__all__" read_only_fields = [ "workspace", - "project" "anchor", + "project", "anchor", + ] + + +class ProjectPublicMemberSerializer(BaseSerializer): + + class Meta: + model = ProjectPublicMember + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "member", ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index b8743476e50..558b7f059d8 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -51,6 +51,7 @@ WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + LeaveWorkspaceEndpoint, ## End Workspaces # File Assets FileAssetEndpoint, @@ -68,6 +69,7 @@ UserProjectInvitationsViewset, ProjectIdentifierEndpoint, ProjectFavoritesViewSet, + LeaveProjectEndpoint, ## End Projects # Issues IssueViewSet, @@ -89,7 +91,6 @@ IssueCommentPublicViewSet, IssueReactionViewSet, CommentReactionViewSet, - ExportIssuesEndpoint, ## End Issues # States StateViewSet, @@ -165,16 +166,23 @@ # Notification NotificationViewSet, UnreadNotificationEndpoint, + MarkAllReadNotificationViewSet, ## End Notification # Public Boards ProjectDeployBoardViewSet, - ProjectDeployBoardIssuesPublicEndpoint, + ProjectIssuesPublicEndpoint, ProjectDeployBoardPublicSettingsEndpoint, IssueReactionPublicViewSet, CommentReactionPublicViewSet, InboxIssuePublicViewSet, IssueVotePublicViewSet, + WorkspaceProjectDeployBoardEndpoint, + IssueRetrievePublicEndpoint, ## End Public Boards + ## Exporter + ExportIssuesEndpoint, + ## End Exporter + ) @@ -231,7 +239,7 @@ UpdateUserTourCompletedEndpoint.as_view(), name="user-tour", ), - path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"), + path("users/workspaces//activities/", UserActivityEndpoint.as_view(), name="user-activities"), # user workspaces path( "users/me/workspaces/", @@ -435,6 +443,11 @@ WorkspaceLabelsEndpoint.as_view(), name="workspace-labels", ), + path( + "workspaces//members/leave/", + LeaveWorkspaceEndpoint.as_view(), + name="workspace-labels", + ), ## End Workspaces ## # Projects path( @@ -548,6 +561,11 @@ ), name="project", ), + path( + "workspaces//projects//members/leave/", + LeaveProjectEndpoint.as_view(), + name="project", + ), # End Projects # States path( @@ -1490,6 +1508,15 @@ UnreadNotificationEndpoint.as_view(), name="unread-notifications", ), + path( + "workspaces//users/notifications/mark-all-read/", + MarkAllReadNotificationViewSet.as_view( + { + "post": "create", + } + ), + name="mark-all-read-notifications", + ), ## End Notification # Public Boards path( @@ -1520,9 +1547,14 @@ ), path( "public/workspaces//project-boards//issues/", - ProjectDeployBoardIssuesPublicEndpoint.as_view(), + ProjectIssuesPublicEndpoint.as_view(), name="project-deploy-board", ), + path( + "public/workspaces//project-boards//issues//", + IssueRetrievePublicEndpoint.as_view(), + name="workspace-project-boards", + ), path( "public/workspaces//project-boards//issues//comments/", IssueCommentPublicViewSet.as_view( @@ -1614,5 +1646,10 @@ ), name="issue-vote-project-board", ), + path( + "public/workspaces//project-boards/", + WorkspaceProjectDeployBoardEndpoint.as_view(), + name="workspace-project-boards", + ), ## End Public Boards ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 11223f90a05..71647bfea8f 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -12,10 +12,11 @@ ProjectUserViewsEndpoint, ProjectMemberUserEndpoint, ProjectFavoritesViewSet, - ProjectDeployBoardIssuesPublicEndpoint, ProjectDeployBoardViewSet, ProjectDeployBoardPublicSettingsEndpoint, ProjectMemberEndpoint, + WorkspaceProjectDeployBoardEndpoint, + LeaveProjectEndpoint, ) from .user import ( UserEndpoint, @@ -52,6 +53,7 @@ WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, WorkspaceMembersEndpoint, + LeaveWorkspaceEndpoint, ) from .state import StateViewSet from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet @@ -84,6 +86,8 @@ IssueReactionPublicViewSet, CommentReactionPublicViewSet, IssueVotePublicViewSet, + IssueRetrievePublicEndpoint, + ProjectIssuesPublicEndpoint, ) from .auth_extended import ( @@ -161,7 +165,7 @@ DefaultAnalyticsEndpoint, ) -from .notification import NotificationViewSet, UnreadNotificationEndpoint +from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet from .exporter import ( ExportIssuesEndpoint, diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py index 0b935a4d369..d9b6e502d1d 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/api/views/asset.py @@ -18,10 +18,21 @@ class FileAssetEndpoint(BaseAPIView): """ def get(self, request, workspace_id, asset_key): - asset_key = str(workspace_id) + "/" + asset_key - files = FileAsset.objects.filter(asset=asset_key) - serializer = FileAssetSerializer(files, context={"request": request}, many=True) - return Response(serializer.data) + try: + asset_key = str(workspace_id) + "/" + asset_key + files = FileAsset.objects.filter(asset=asset_key) + if files.exists(): + serializer = FileAssetSerializer(files, context={"request": request}, many=True) + return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) + else: + return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + def post(self, request, slug): try: @@ -68,11 +79,16 @@ class UserAssetsEndpoint(BaseAPIView): def get(self, request, asset_key): try: files = FileAsset.objects.filter(asset=asset_key, created_by=request.user) - serializer = FileAssetSerializer(files, context={"request": request}) - return Response(serializer.data) - except FileAsset.DoesNotExist: + if files.exists(): + serializer = FileAssetSerializer(files, context={"request": request}) + return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) + else: + return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) return Response( - {"error": "File Asset does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, ) def post(self, request): diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index 3c260e03b91..60b0ec0c62c 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -1,24 +1,41 @@ +# Python imports +import zoneinfo + # Django imports from django.urls import resolve from django.conf import settings - +from django.utils import timezone # Third part imports + from rest_framework import status from rest_framework.viewsets import ModelViewSet from rest_framework.exceptions import APIException from rest_framework.views import APIView from rest_framework.filters import SearchFilter from rest_framework.permissions import IsAuthenticated -from rest_framework.exceptions import NotFound from sentry_sdk import capture_exception from django_filters.rest_framework import DjangoFilterBackend # Module imports -from plane.db.models import Workspace, Project from plane.utils.paginator import BasePaginator -class BaseViewSet(ModelViewSet, BasePaginator): +class TimezoneMixin: + """ + This enables timezone conversion according + to the user set timezone + """ + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + if request.user.is_authenticated: + timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone)) + else: + timezone.deactivate() + + + + +class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): model = None @@ -67,7 +84,7 @@ def project_id(self): return self.kwargs.get("pk", None) -class BaseAPIView(APIView, BasePaginator): +class BaseAPIView(TimezoneMixin, APIView, BasePaginator): permission_classes = [ IsAuthenticated, diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index a3d89fa8182..253da2c5b98 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -191,11 +191,10 @@ def list(self, request, slug, project_id): 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 @@ def list(self, request, slug, project_id): filter=Q(completed_at__isnull=True), ) ) - .order_by("first_name", "last_name") + .order_by("display_name") ) label_distribution = ( @@ -334,13 +333,21 @@ def partial_update(self, request, slug, project_id, pk): 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 @@ def retrieve(self, request, slug, project_id, pk): .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 @@ def post(self, request, slug, project_id): class CycleFavoriteViewSet(BaseViewSet): - serializer_class = CycleFavoriteSerializer model = CycleFavorite diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 6f0f1e6ae6b..a390f7b8109 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -17,17 +17,19 @@ 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 # Third Party imports from rest_framework.response import Response from rest_framework import status from rest_framework.parsers import MultiPartParser, FormParser +from rest_framework.permissions import AllowAny, IsAuthenticated from sentry_sdk import capture_exception # Module imports @@ -49,6 +51,7 @@ IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssuePublicSerializer, ) from plane.api.permissions import ( WorkspaceEntityPermission, @@ -73,10 +76,12 @@ CommentReaction, ProjectDeployBoard, IssueVote, + ProjectPublicMember, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters +from plane.bgtasks.export_task import issue_export_task class IssueViewSet(BaseViewSet): @@ -333,7 +338,11 @@ def get(self, request, slug): issue_queryset = ( Issue.issue_objects.filter( - (Q(assignees__in=[request.user]) | Q(created_by=request.user)), + ( + Q(assignees__in=[request.user]) + | Q(created_by=request.user) + | Q(issue_subscribers__subscriber=request.user) + ), workspace__slug=slug, ) .annotate( @@ -482,7 +491,7 @@ def get(self, request, slug, project_id, issue_id): issue_activities = ( IssueActivity.objects.filter(issue_id=issue_id) .filter( - ~Q(field="comment"), + ~Q(field__in=["comment", "vote", "reaction"]), project__project_projectmember__member=self.request.user, ) .select_related("actor", "workspace", "issue", "project") @@ -492,6 +501,12 @@ def get(self, request, slug, project_id, issue_id): .filter(project__project_projectmember__member=self.request.user) .order_by("created_at") .select_related("actor", "issue", "project", "workspace") + .prefetch_related( + Prefetch( + "comment_reactions", + queryset=CommentReaction.objects.select_related("actor"), + ) + ) ) issue_activities = IssueActivitySerializer(issue_activities, many=True).data issue_comments = IssueCommentSerializer(issue_comments, many=True).data @@ -588,6 +603,15 @@ def get_queryset(self): .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() ) @@ -769,7 +793,9 @@ def get(self, request, slug, project_id, issue_id): .order_by("state_group") ) - result = {item["state_group"]: item["state_count"] for item in state_distribution} + result = { + item["state_group"]: item["state_count"] for item in state_distribution + } serializer = IssueLiteSerializer( sub_issues, @@ -1384,6 +1410,14 @@ def perform_create(self, serializer): project_id=self.kwargs.get("project_id"), actor=self.request.user, ) + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) def destroy(self, request, slug, project_id, issue_id, reaction_code): try: @@ -1394,6 +1428,19 @@ def destroy(self, request, slug, project_id, issue_id, reaction_code): reaction=reaction_code, actor=request.user, ) + issue_activity.delay( + type="issue_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(issue_reaction.id), + } + ), + ) issue_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) except IssueReaction.DoesNotExist: @@ -1434,6 +1481,14 @@ def perform_create(self, serializer): comment_id=self.kwargs.get("comment_id"), project_id=self.kwargs.get("project_id"), ) + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) def destroy(self, request, slug, project_id, comment_id, reaction_code): try: @@ -1444,6 +1499,20 @@ def destroy(self, request, slug, project_id, comment_id, reaction_code): reaction=reaction_code, actor=request.user, ) + issue_activity.delay( + type="comment_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(comment_reaction.id), + "comment_id": str(comment_id), + } + ), + ) comment_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) except CommentReaction.DoesNotExist: @@ -1468,23 +1537,48 @@ class IssueCommentPublicViewSet(BaseViewSet): "workspace__id", ] - 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")) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .distinct() - ) + def get_permissions(self): + if self.action in ["list", "retrieve"]: + self.permission_classes = [ + AllowAny, + ] else: + self.permission_classes = [ + IsAuthenticated, + ] + + return super(IssueCommentPublicViewSet, self).get_permissions() + + def get_queryset(self): + 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: + return IssueComment.objects.none() + except ProjectDeployBoard.DoesNotExist: return IssueComment.objects.none() def create(self, request, slug, project_id, issue_id): @@ -1499,21 +1593,13 @@ def create(self, request, slug, project_id, issue_id): status=status.HTTP_400_BAD_REQUEST, ) - access = ( - "INTERNAL" - if ProjectMember.objects.filter( - project_id=project_id, member=request.user - ).exists() - else "EXTERNAL" - ) - serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): serializer.save( project_id=project_id, issue_id=issue_id, actor=request.user, - access=access, + access="EXTERNAL", ) issue_activity.delay( type="comment.activity.created", @@ -1523,6 +1609,16 @@ def create(self, request, slug, project_id, issue_id): project_id=str(project_id), current_instance=None, ) + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: @@ -1567,7 +1663,8 @@ def partial_update(self, request, slug, project_id, issue_id, pk): except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist): return Response( {"error": "IssueComent Does not exists"}, - status=status.HTTP_400_BAD_REQUEST,) + status=status.HTTP_400_BAD_REQUEST, + ) def destroy(self, request, slug, project_id, issue_id, pk): try: @@ -1614,21 +1711,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() - ) - else: + try: + 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() + ) + else: + return IssueReaction.objects.none() + except ProjectDeployBoard.DoesNotExist: return IssueReaction.objects.none() def create(self, request, slug, project_id, issue_id): @@ -1648,6 +1748,23 @@ def create(self, request, slug, project_id, issue_id): serializer.save( project_id=project_id, issue_id=issue_id, actor=request.user ) + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except ProjectDeployBoard.DoesNotExist: @@ -1679,6 +1796,19 @@ def destroy(self, request, slug, project_id, issue_id, reaction_code): reaction=reaction_code, actor=request.user, ) + issue_activity.delay( + type="issue_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(issue_reaction.id), + } + ), + ) issue_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) except IssueReaction.DoesNotExist: @@ -1699,21 +1829,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() - ) - else: + try: + 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() + ) + else: + return CommentReaction.objects.none() + except ProjectDeployBoard.DoesNotExist: return CommentReaction.objects.none() def create(self, request, slug, project_id, comment_id): @@ -1733,8 +1866,29 @@ def create(self, request, slug, project_id, comment_id): serializer.save( project_id=project_id, comment_id=comment_id, actor=request.user ) + if not ProjectMember.objects.filter( + project_id=project_id, member=request.user + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except IssueComment.DoesNotExist: + return Response( + {"error": "Comment does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) except ProjectDeployBoard.DoesNotExist: return Response( {"error": "Project board does not exist"}, @@ -1765,6 +1919,20 @@ def destroy(self, request, slug, project_id, comment_id, reaction_code): reaction=reaction_code, actor=request.user, ) + issue_activity.delay( + type="comment_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(comment_reaction.id), + "comment_id": str(comment_id), + } + ), + ) comment_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) except CommentReaction.DoesNotExist: @@ -1785,13 +1953,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: @@ -1799,10 +1977,31 @@ def create(self, request, slug, project_id, issue_id): actor_id=request.user.id, project_id=project_id, issue_id=issue_id, - vote=request.data.get("vote", 1), + ) + # Add the user for workspace tracking + if not ProjectMember.objects.filter( + project_id=project_id, member=request.user + ).exists(): + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + issue_vote.vote = request.data.get("vote", 1) + issue_vote.save() + issue_activity.delay( + type="issue_vote.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, ) 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( @@ -1818,6 +2017,19 @@ def destroy(self, request, slug, project_id, issue_id): issue_id=issue_id, actor_id=request.user.id, ) + issue_activity.delay( + type="issue_vote.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "vote": str(issue_vote.vote), + "identifier": str(issue_vote.id), + } + ), + ) issue_vote.delete() return Response(status=status.HTTP_204_NO_CONTENT) except Exception as e: @@ -1828,27 +2040,196 @@ def destroy(self, request, slug, project_id, issue_id): ) -class ExportIssuesEndpoint(BaseAPIView): +class IssueRetrievePublicEndpoint(BaseAPIView): permission_classes = [ - WorkSpaceAdminPermission, + AllowAny, ] - def post(self, request, slug): + def get(self, request, slug, project_id, issue_id): try: + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=issue_id + ) + serializer = IssuePublicSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + except Issue.DoesNotExist: + return Response( + {"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + print(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) - issue_export_task.delay( - email=request.user.email, data=request.data, slug=slug ,exporter_name=request.user.first_name + +class ProjectIssuesPublicEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id ) + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", None] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .select_related("project", "workspace", "state", "parent") + .prefetch_related("assignees", "labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issues = IssuePublicSerializer(issue_queryset, many=True).data + + state_group_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + states = ( + State.objects.filter( + 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 + ).values("id", "name", "color", "parent") + + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + issues = group_results(issues, group_by) + return Response( { - "message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}" + "issues": issues, + "states": states, + "labels": labels, }, status=status.HTTP_200_OK, ) + except ProjectDeployBoard.DoesNotExist: + return Response( + {"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND + ) except Exception as e: capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, - ) \ No newline at end of file + ) diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/api/views/notification.py index 2abc82631de..75b94f034bd 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/api/views/notification.py @@ -10,7 +10,13 @@ # Module imports from .base import BaseViewSet, BaseAPIView -from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue, WorkspaceMember +from plane.db.models import ( + Notification, + IssueAssignee, + IssueSubscriber, + Issue, + WorkspaceMember, +) from plane.api.serializers import NotificationSerializer @@ -83,13 +89,17 @@ def list(self, request, slug): # Created issues if type == "created": - if WorkspaceMember.objects.filter(workspace__slug=slug, member=request.user, role__lt=15).exists(): + if WorkspaceMember.objects.filter( + workspace__slug=slug, member=request.user, role__lt=15 + ).exists(): notifications = Notification.objects.none() else: issue_ids = Issue.objects.filter( workspace__slug=slug, created_by=request.user ).values_list("pk", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) # Pagination if request.GET.get("per_page", False) and request.GET.get("cursor", False): @@ -274,3 +284,80 @@ def get(self, request, slug): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class MarkAllReadNotificationViewSet(BaseViewSet): + def create(self, request, slug): + try: + snoozed = request.data.get("snoozed", False) + archived = request.data.get("archived", False) + type = request.data.get("type", "all") + + notifications = ( + Notification.objects.filter( + workspace__slug=slug, + receiver_id=request.user.id, + read_at__isnull=True, + ) + .select_related("workspace", "project", "triggered_by", "receiver") + .order_by("snoozed_till", "-created_at") + ) + + # Filter for snoozed notifications + if snoozed: + notifications = notifications.filter( + Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False) + ) + else: + notifications = notifications.filter( + Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + ) + + # Filter for archived or unarchive + if archived: + notifications = notifications.filter(archived_at__isnull=False) + else: + notifications = notifications.filter(archived_at__isnull=True) + + # Subscribed issues + if type == "watching": + issue_ids = IssueSubscriber.objects.filter( + workspace__slug=slug, subscriber_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_identifier__in=issue_ids) + + # Assigned Issues + if type == "assigned": + issue_ids = IssueAssignee.objects.filter( + workspace__slug=slug, assignee_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_identifier__in=issue_ids) + + # Created issues + if type == "created": + if WorkspaceMember.objects.filter( + workspace__slug=slug, member=request.user, role__lt=15 + ).exists(): + notifications = Notification.objects.none() + else: + issue_ids = Issue.objects.filter( + workspace__slug=slug, created_by=request.user + ).values_list("pk", flat=True) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) + + updated_notifications = [] + for notification in notifications: + notification.read_at = timezone.now() + updated_notifications.append(notification) + Notification.objects.bulk_update( + updated_notifications, ["read_at"], batch_size=100 + ) + return Response({"message": "Successful"}, status=status.HTTP_200_OK) + 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/project.py b/apiserver/plane/api/views/project.py index 6adee0016f5..a83bbca25d4 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -11,14 +11,8 @@ 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 @@ ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, + ProjectLitePermission, ) from plane.db.models import ( @@ -71,16 +66,9 @@ 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): @@ -287,7 +275,10 @@ def create(self, request, slug): ) data = serializer.data + # Additional fields of the member data["sort_order"] = project_member.sort_order + data["member_role"] = project_member.role + data["is_member"] = True return Response(data, status=status.HTTP_201_CREATED) return Response( serializer.errors, @@ -626,7 +617,7 @@ def destroy(self, request, slug, project_id, pk): 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) @@ -1140,145 +1131,78 @@ def get(self, request, slug, project_id): ) -class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView): +class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] - def get(self, request, slug, project_id): + def get(self, request, slug): try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - filters = issue_filters(request.query_params, "GET") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - Issue.issue_objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(project_id=project_id) - .filter(workspace__slug=slug) - .select_related("project", "workspace", "state", "parent") - .prefetch_related("assignees", "labels") - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) - ) - .filter(**filters) - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate(module_id=F("issue_module__module_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) + projects = ( + Project.objects.filter(workspace__slug=slug) .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") + is_public=Exists( + ProjectDeployBoard.objects.filter( + workspace__slug=slug, project_id=OuterRef("pk") + ) ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") ) + .filter(is_public=True) + ).values( + "id", + "identifier", + "name", + "description", + "emoji", + "icon_prop", + "cover_image", + ) + + return Response(projects, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, ) - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issues = IssueLiteSerializer(issue_queryset, many=True).data - states = State.objects.filter( - workspace__slug=slug, project_id=project_id - ).values("name", "group", "color", "id") - - labels = Label.objects.filter( - workspace__slug=slug, project_id=project_id - ).values("id", "name", "color", "parent") - - ## Grouping the results - group_by = request.GET.get("group_by", False) - if group_by: - issues = group_results(issues, group_by) +class LeaveProjectEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] - return Response( - { - "issues": issues, - "states": states, - "labels": labels, - }, - status=status.HTTP_200_OK, + def delete(self, request, slug, project_id): + try: + project_member = ProjectMember.objects.get( + workspace__slug=slug, + member=request.user, + project_id=project_id, ) - except ProjectDeployBoard.DoesNotExist: + + # 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": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND + {"error": "Workspace member does not exists"}, + status=status.HTTP_400_BAD_REQUEST, ) except Exception as e: capture_exception(e) diff --git a/apiserver/plane/api/views/user.py b/apiserver/plane/api/views/user.py index 84ee47e4258..68958e5041c 100644 --- a/apiserver/plane/api/views/user.py +++ b/apiserver/plane/api/views/user.py @@ -137,11 +137,11 @@ def patch(self, request): class UserActivityEndpoint(BaseAPIView, BasePaginator): - def get(self, request): + def get(self, request, slug): try: - queryset = IssueActivity.objects.filter(actor=request.user).select_related( - "actor", "workspace", "issue", "project" - ) + queryset = IssueActivity.objects.filter( + actor=request.user, workspace__slug=slug + ).select_related("actor", "workspace", "issue", "project") return self.paginate( request=request, diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index cfdd0dd9bf0..ce425185e6e 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1100,7 +1100,6 @@ def get(self, request, slug, user_id): created_issues = ( Issue.issue_objects.filter( workspace__slug=slug, - assignees__in=[user_id], project__project_projectmember__member=request.user, created_by_id=user_id, ) @@ -1198,6 +1197,7 @@ def get(self, request, slug, user_id): projects = request.query_params.getlist("project", []) queryset = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction"]), workspace__slug=slug, project__project_projectmember__member=request.user, actor=user_id, @@ -1473,3 +1473,44 @@ def get(self, request, slug): {"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/export_task.py b/apiserver/plane/bgtasks/export_task.py index 22a9afe51bc..a45120eb5dd 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -4,6 +4,7 @@ import json import boto3 import zipfile +from urllib.parse import urlparse, urlunparse # Django imports from django.conf import settings @@ -23,9 +24,11 @@ def dateTimeConverter(time): if time: return time.strftime("%a, %d %b %Y %I:%M:%S %Z%z") + def dateConverter(time): if time: - return time.strftime("%a, %d %b %Y") + return time.strftime("%a, %d %b %Y") + def create_csv_file(data): csv_buffer = io.StringIO() @@ -66,28 +69,53 @@ def create_zip_file(files): def upload_to_s3(zip_file, workspace_id, token_id, slug): - s3 = boto3.client( - "s3", - region_name=settings.AWS_REGION, - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - config=Config(signature_version="s3v4"), - ) file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip" + expires_in = 7 * 24 * 60 * 60 - s3.upload_fileobj( - zip_file, - settings.AWS_S3_BUCKET_NAME, - file_name, - ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"}, - ) + if settings.DOCKERIZED and settings.USE_MINIO: + s3 = boto3.client( + "s3", + endpoint_url=settings.AWS_S3_ENDPOINT_URL, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) + s3.upload_fileobj( + zip_file, + settings.AWS_STORAGE_BUCKET_NAME, + file_name, + ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"}, + ) + presigned_url = s3.generate_presigned_url( + "get_object", + Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name}, + ExpiresIn=expires_in, + ) + # Create the new url with updated domain and protocol + presigned_url = presigned_url.replace( + "http://plane-minio:9000/uploads/", + f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/", + ) + else: + s3 = boto3.client( + "s3", + region_name=settings.AWS_REGION, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) + s3.upload_fileobj( + zip_file, + settings.AWS_S3_BUCKET_NAME, + file_name, + ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"}, + ) - expires_in = 7 * 24 * 60 * 60 - presigned_url = s3.generate_presigned_url( - "get_object", - Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name}, - ExpiresIn=expires_in, - ) + presigned_url = s3.generate_presigned_url( + "get_object", + Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name}, + ExpiresIn=expires_in, + ) exporter_instance = ExporterHistory.objects.get(token=token_id) @@ -98,7 +126,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug): else: exporter_instance.status = "failed" - exporter_instance.save(update_fields=["status", "url","key"]) + exporter_instance.save(update_fields=["status", "url", "key"]) def generate_table_row(issue): @@ -145,7 +173,7 @@ def generate_json_row(issue): else "", "Labels": issue["labels__name"], "Cycle Name": issue["issue_cycle__cycle__name"], - "Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]), + "Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]), "Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]), "Module Name": issue["issue_module__module__name"], "Module Start Date": dateConverter(issue["issue_module__module__start_date"]), @@ -242,7 +270,9 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s workspace_issues = ( ( Issue.objects.filter( - workspace__id=workspace_id, project_id__in=project_ids + workspace__id=workspace_id, + project_id__in=project_ids, + project__project_projectmember__member=exporter_instance.initiated_by_id, ) .select_related("project", "workspace", "state", "parent", "created_by") .prefetch_related( @@ -275,7 +305,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s "labels__name", ) ) - .order_by("project__identifier","sequence_id") + .order_by("project__identifier", "sequence_id") .distinct() ) # CSV header @@ -338,7 +368,6 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s exporter_instance.status = "failed" exporter_instance.reason = str(e) exporter_instance.save(update_fields=["status", "reason"]) - # Print logs if in DEBUG mode if settings.DEBUG: print(e) diff --git a/apiserver/plane/bgtasks/exporter_expired_task.py b/apiserver/plane/bgtasks/exporter_expired_task.py index 79990434795..a77d68b4b55 100644 --- a/apiserver/plane/bgtasks/exporter_expired_task.py +++ b/apiserver/plane/bgtasks/exporter_expired_task.py @@ -21,18 +21,29 @@ def delete_old_s3_link(): expired_exporter_history = ExporterHistory.objects.filter( Q(url__isnull=False) & Q(created_at__lte=timezone.now() - timedelta(days=8)) ).values_list("key", "id") - - s3 = boto3.client( - "s3", - region_name="ap-south-1", - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - config=Config(signature_version="s3v4"), - ) + if settings.DOCKERIZED and settings.USE_MINIO: + s3 = boto3.client( + "s3", + endpoint_url=settings.AWS_S3_ENDPOINT_URL, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) + else: + s3 = boto3.client( + "s3", + region_name="ap-south-1", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) for file_name, exporter_id in expired_exporter_history: # Delete object from S3 if file_name: - s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name) + if settings.DOCKERIZED and settings.USE_MINIO: + s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name) + else: + s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name) ExporterHistory.objects.filter(id=exporter_id).update(url=None) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 1cc6c85cc9a..0cadac55323 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -24,6 +24,9 @@ IssueSubscriber, Notification, IssueAssignee, + IssueReaction, + CommentReaction, + IssueComment, ) from plane.api.serializers import IssueActivitySerializer @@ -629,7 +632,7 @@ def update_issue_activity( "parent": track_parent, "priority": track_priority, "state": track_state, - "description": track_description, + "description_html": track_description, "target_date": track_target_date, "start_date": track_start_date, "labels_list": track_labels, @@ -1022,6 +1025,150 @@ def delete_attachment_activity( ) ) +def create_issue_reaction_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + requested_data = json.loads(requested_data) if requested_data is not None else None + if requested_data and requested_data.get("reaction") is not None: + issue_reaction = IssueReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', flat=True).first() + if issue_reaction is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="created", + old_value=None, + new_value=requested_data.get("reaction"), + field="reaction", + project=project, + workspace=project.workspace, + comment="added the reaction", + old_identifier=None, + new_identifier=issue_reaction, + ) + ) + + +def delete_issue_reaction_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance and current_instance.get("reaction") is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="deleted", + old_value=current_instance.get("reaction"), + new_value=None, + field="reaction", + project=project, + workspace=project.workspace, + comment="removed the reaction", + old_identifier=current_instance.get("identifier"), + new_identifier=None, + ) + ) + + +def create_comment_reaction_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + requested_data = json.loads(requested_data) if requested_data is not None else None + if requested_data and requested_data.get("reaction") is not None: + comment_reaction_id, comment_id = CommentReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', 'comment__id').first() + comment = IssueComment.objects.get(pk=comment_id,project=project) + if comment is not None and comment_reaction_id is not None and comment_id is not None: + issue_activities.append( + IssueActivity( + issue_id=comment.issue_id, + actor=actor, + verb="created", + old_value=None, + new_value=requested_data.get("reaction"), + field="reaction", + project=project, + workspace=project.workspace, + comment="added the reaction", + old_identifier=None, + new_identifier=comment_reaction_id, + ) + ) + + +def delete_comment_reaction_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance and current_instance.get("reaction") is not None: + issue_id = IssueComment.objects.filter(pk=current_instance.get("comment_id"), project=project).values_list('issue_id', flat=True).first() + if issue_id is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="deleted", + old_value=current_instance.get("reaction"), + new_value=None, + field="reaction", + project=project, + workspace=project.workspace, + comment="removed the reaction", + old_identifier=current_instance.get("identifier"), + new_identifier=None, + ) + ) + + +def create_issue_vote_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + requested_data = json.loads(requested_data) if requested_data is not None else None + if requested_data and requested_data.get("vote") is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="created", + old_value=None, + new_value=requested_data.get("vote"), + field="vote", + project=project, + workspace=project.workspace, + comment="added the vote", + old_identifier=None, + new_identifier=None, + ) + ) + + +def delete_issue_vote_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance and current_instance.get("vote") is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="deleted", + old_value=current_instance.get("vote"), + new_value=None, + field="vote", + project=project, + workspace=project.workspace, + comment="removed the vote", + old_identifier=current_instance.get("identifier"), + new_identifier=None, + ) + ) + # Receive message from room group @shared_task @@ -1045,6 +1192,12 @@ def issue_activity( "cycle.activity.deleted", "module.activity.created", "module.activity.deleted", + "issue_reaction.activity.created", + "issue_reaction.activity.deleted", + "comment_reaction.activity.created", + "comment_reaction.activity.deleted", + "issue_vote.activity.created", + "issue_vote.activity.deleted", ]: issue = Issue.objects.filter(pk=issue_id).first() @@ -1080,6 +1233,12 @@ def issue_activity( "link.activity.deleted": delete_link_activity, "attachment.activity.created": create_attachment_activity, "attachment.activity.deleted": delete_attachment_activity, + "issue_reaction.activity.created": create_issue_reaction_activity, + "issue_reaction.activity.deleted": delete_issue_reaction_activity, + "comment_reaction.activity.created": create_comment_reaction_activity, + "comment_reaction.activity.deleted": delete_comment_reaction_activity, + "issue_vote.activity.created": create_issue_vote_activity, + "issue_vote.activity.deleted": delete_issue_vote_activity, } func = ACTIVITY_MAPPER.get(type) @@ -1119,6 +1278,12 @@ def issue_activity( "cycle.activity.deleted", "module.activity.created", "module.activity.deleted", + "issue_reaction.activity.created", + "issue_reaction.activity.deleted", + "comment_reaction.activity.created", + "comment_reaction.activity.deleted", + "issue_vote.activity.created", + "issue_vote.activity.deleted", ]: # Create Notifications bulk_notifications = [] diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index 0e3ead65ddf..a1f4a3e920e 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -64,7 +64,7 @@ def archive_old_issues(): issues_to_update.append(issue) # Bulk Update the issues and log the activity - Issue.objects.bulk_update( + updated_issues = Issue.objects.bulk_update( issues_to_update, ["archived_at"], batch_size=100 ) [ @@ -77,7 +77,7 @@ def archive_old_issues(): current_instance=None, subscriber=False, ) - for issue in issues_to_update + for issue in updated_issues ] return except Exception as e: @@ -136,7 +136,7 @@ def close_old_issues(): issues_to_update.append(issue) # Bulk Update the issues and log the activity - Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) + updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) [ issue_activity.delay( type="issue.activity.updated", @@ -147,7 +147,7 @@ def close_old_issues(): current_instance=None, subscriber=False, ) - for issue in issues_to_update + for issue in updated_issues ] return except Exception as e: 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 new file mode 100644 index 00000000000..01af46d20cf --- /dev/null +++ b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 4.2.3 on 2023-08-29 06:26 + +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") + updated_users = [] + for obj in UserModel.objects.all(): + obj.user_timezone = "UTC" + updated_users.append(obj) + UserModel.objects.bulk_update(updated_users, ["user_timezone"], batch_size=100) + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0041_cycle_sort_order_issuecomment_access_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='user_timezone', + field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'America/Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fort_Wayne', 'America/Fort_Wayne'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Knox_IN', 'America/Knox_IN'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Rosario', 'America/Rosario'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/South_Pole', 'Antarctica/South_Pole'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Ashkhabad', 'Asia/Ashkhabad'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Chungking', 'Asia/Chungking'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Dacca', 'Asia/Dacca'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macao', 'Asia/Macao'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimbu', 'Asia/Thimbu'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ujung_Pandang', 'Asia/Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Ulan_Bator', 'Asia/Ulan_Bator'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/ACT', 'Australia/ACT'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/LHI', 'Australia/LHI'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/NSW', 'Australia/NSW'), ('Australia/North', 'Australia/North'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Queensland', 'Australia/Queensland'), ('Australia/South', 'Australia/South'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Tasmania', 'Australia/Tasmania'), ('Australia/Victoria', 'Australia/Victoria'), ('Australia/West', 'Australia/West'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Brazil/Acre', 'Brazil/Acre'), ('Brazil/DeNoronha', 'Brazil/DeNoronha'), ('Brazil/East', 'Brazil/East'), ('Brazil/West', 'Brazil/West'), ('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Canada/Saskatchewan', 'Canada/Saskatchewan'), ('Canada/Yukon', 'Canada/Yukon'), ('Chile/Continental', 'Chile/Continental'), ('Chile/EasterIsland', 'Chile/EasterIsland'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('GMT', 'GMT'), ('GMT+0', 'GMT+0'), ('GMT-0', 'GMT-0'), ('GMT0', 'GMT0'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('Mexico/BajaNorte', 'Mexico/BajaNorte'), ('Mexico/BajaSur', 'Mexico/BajaSur'), ('Mexico/General', 'Mexico/General'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('US/Alaska', 'US/Alaska'), ('US/Aleutian', 'US/Aleutian'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/East-Indiana', 'US/East-Indiana'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Indiana-Starke', 'US/Indiana-Starke'), ('US/Michigan', 'US/Michigan'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('US/Samoa', 'US/Samoa'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')], default='UTC', max_length=255), + ), + migrations.AlterField( + model_name='issuelink', + name='title', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.RunPython(update_user_timezones), + migrations.AlterField( + model_name='issuevote', + name='vote', + field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1), + ), + 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/models/__init__.py b/apiserver/plane/db/models/__init__.py index 659eea3eb38..90532dc64e3 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -19,6 +19,7 @@ ProjectIdentifier, ProjectFavorite, ProjectDeployBoard, + ProjectPublicMember, ) from .issue import ( diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 7af9e6e1423..78e95838025 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -293,7 +293,7 @@ class IssueComment(ProjectBaseModel): comment_json = models.JSONField(blank=True, default=dict) comment_html = models.TextField(blank=True, default="

") attachments = ArrayField(models.URLField(), size=10, blank=True, default=list) - issue = models.ForeignKey(Issue, on_delete=models.CASCADE) + issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments") # System can also create comment actor = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -476,10 +476,12 @@ class IssueVote(ProjectBaseModel): choices=( (-1, "DOWNVOTE"), (1, "UPVOTE"), - ) + ), + default=1, ) + class Meta: - unique_together = ["issue", "actor"] + unique_together = ["issue", "actor",] verbose_name = "Issue Vote" verbose_name_plural = "Issue Votes" db_table = "issue_votes" diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 0c2b5cb96db..da155af4029 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -254,3 +254,18 @@ class Meta: def __str__(self): """Return project and anchor""" return f"{self.anchor} <{self.project.name}>" + + +class ProjectPublicMember(ProjectBaseModel): + member = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="public_project_members", + ) + + class Meta: + unique_together = ["project", "member"] + verbose_name = "Project Public Member" + verbose_name_plural = "Project Public Members" + db_table = "project_public_members" + ordering = ("-created_at",) diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 3975a3b9311..e90e19c5eee 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -2,6 +2,7 @@ import uuid import string import random +import pytz # Django imports from django.db import models @@ -9,9 +10,6 @@ from django.dispatch import receiver from django.contrib.auth.models import AbstractBaseUser, UserManager, PermissionsMixin from django.utils import timezone -from django.core.mail import EmailMultiAlternatives -from django.template.loader import render_to_string -from django.utils.html import strip_tags from django.conf import settings # Third party imports @@ -66,7 +64,8 @@ class User(AbstractBaseUser, PermissionsMixin): billing_address = models.JSONField(null=True) has_billing_address = models.BooleanField(default=False) - user_timezone = models.CharField(max_length=255, default="Asia/Kolkata") + USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) + user_timezone = models.CharField(max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES) last_active = models.DateTimeField(default=timezone.now, null=True) last_login_time = models.DateTimeField(null=True) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 59e0bd31b2b..27da44d9cf0 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -49,7 +49,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", "crum.CurrentRequestUserMiddleware", "django.middleware.gzip.GZipMiddleware", -] + ] REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( @@ -161,7 +161,7 @@ LANGUAGE_CODE = "en-us" -TIME_ZONE = "Asia/Kolkata" +TIME_ZONE = "UTC" USE_I18N = True diff --git a/apiserver/runtime.txt b/apiserver/runtime.txt index fefae710dc4..d5831c54fb3 100644 --- a/apiserver/runtime.txt +++ b/apiserver/runtime.txt @@ -1 +1 @@ -python-3.11.4 \ No newline at end of file +python-3.11.5 \ No newline at end of file diff --git a/apps/app/components/core/filters/index.ts b/apps/app/components/core/filters/index.ts deleted file mode 100644 index 01c37191167..00000000000 --- a/apps/app/components/core/filters/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./due-date-filter-modal"; -export * from "./due-date-filter-select"; -export * from "./filters-list"; -export * from "./issues-view-filter"; diff --git a/apps/app/components/core/views/spreadsheet-view/single-issue.tsx b/apps/app/components/core/views/spreadsheet-view/single-issue.tsx deleted file mode 100644 index 7e8fd0d26bc..00000000000 --- a/apps/app/components/core/views/spreadsheet-view/single-issue.tsx +++ /dev/null @@ -1,368 +0,0 @@ -import React, { useCallback, useState } from "react"; - -import Link from "next/link"; -import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// components -import { - ViewAssigneeSelect, - ViewDueDateSelect, - ViewEstimateSelect, - ViewIssueLabel, - ViewPrioritySelect, - ViewStartDateSelect, - ViewStateSelect, -} from "components/issues"; -import { Popover2 } from "@blueprintjs/popover2"; -// icons -import { Icon } from "components/ui"; -import { - EllipsisHorizontalIcon, - LinkIcon, - PencilIcon, - TrashIcon, -} from "@heroicons/react/24/outline"; -// hooks -import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; -import useToast from "hooks/use-toast"; -// services -import issuesService from "services/issues.service"; -// constant -import { - CYCLE_DETAILS, - CYCLE_ISSUES_WITH_PARAMS, - MODULE_DETAILS, - MODULE_ISSUES_WITH_PARAMS, - PROJECT_ISSUES_LIST_WITH_PARAMS, - SUB_ISSUES, - VIEW_ISSUES, -} from "constants/fetch-keys"; -// types -import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types"; -// helper -import { copyTextToClipboard } from "helpers/string.helper"; -import { renderLongDetailDateFormat } from "helpers/date-time.helper"; - -type Props = { - issue: IIssue; - index: number; - expanded: boolean; - handleToggleExpand: (issueId: string) => void; - properties: Properties; - handleEditIssue: (issue: IIssue) => void; - handleDeleteIssue: (issue: IIssue) => void; - gridTemplateColumns: string; - disableUserActions: boolean; - user: ICurrentUserResponse | undefined; - userAuth: UserAuth; - nestingLevel: number; -}; - -export const SingleSpreadsheetIssue: React.FC = ({ - issue, - index, - expanded, - handleToggleExpand, - properties, - handleEditIssue, - handleDeleteIssue, - gridTemplateColumns, - disableUserActions, - user, - userAuth, - nestingLevel, -}) => { - const [isOpen, setIsOpen] = useState(false); - const router = useRouter(); - - const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; - - const { params } = useSpreadsheetIssuesView(); - - const { setToastAlert } = useToast(); - - const partialUpdateIssue = useCallback( - (formData: Partial, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; - - const fetchKey = cycleId - ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) - : moduleId - ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) - : viewId - ? VIEW_ISSUES(viewId.toString(), params) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); - - if (issue.parent) { - mutate( - SUB_ISSUES(issue.parent.toString()), - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - sub_issues: (prevData.sub_issues ?? []).map((i) => { - if (i.id === issue.id) { - return { - ...i, - ...formData, - }; - } - return i; - }), - }; - }, - false - ); - } else { - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((p) => { - if (p.id === issue.id) { - return { - ...p, - ...formData, - }; - } - return p; - }), - false - ); - } - - issuesService - .patchIssue( - workspaceSlug as string, - projectId as string, - issue.id as string, - formData, - user - ) - .then(() => { - if (issue.parent) { - mutate(SUB_ISSUES(issue.parent as string)); - } else { - mutate(fetchKey); - - if (cycleId) mutate(CYCLE_DETAILS(cycleId as string)); - if (moduleId) mutate(MODULE_DETAILS(moduleId as string)); - } - }) - .catch((error) => { - console.log(error); - }); - }, - [workspaceSlug, projectId, cycleId, moduleId, viewId, params, user] - ); - - const handleCopyText = () => { - const originURL = - typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard( - `${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}` - ).then(() => { - setToastAlert({ - type: "success", - title: "Link Copied!", - message: "Issue link copied to clipboard.", - }); - }); - }; - - const paddingLeft = `${nestingLevel * 68}px`; - - const tooltipPosition = index === 0 ? "bottom" : "top"; - - const isNotAllowed = userAuth.isGuest || userAuth.isViewer; - - return ( -
-
-
-
- {properties.key && ( - - {issue.project_detail?.identifier}-{issue.sequence_id} - - )} - {!isNotAllowed && !disableUserActions && ( -
- setIsOpen(nextOpenState)} - content={ -
- - - - - -
- } - placement="bottom-start" - > - -
-
- )} -
- - {issue.sub_issues_count > 0 && ( -
- -
- )} -
- - - - {issue.name} - - -
- {properties.state && ( -
- -
- )} - {properties.priority && ( -
- -
- )} - {properties.assignee && ( -
- -
- )} - {properties.labels && ( -
- -
- )} - - {properties.start_date && ( -
- -
- )} - - {properties.due_date && ( -
- -
- )} - {properties.estimate && ( -
- -
- )} - {properties.created_on && ( -
- {renderLongDetailDateFormat(issue.created_at)} -
- )} - {properties.updated_on && ( -
- {renderLongDetailDateFormat(issue.updated_at)} -
- )} -
- ); -}; diff --git a/apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx deleted file mode 100644 index a4f426a2369..00000000000 --- a/apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useState } from "react"; - -// next -import { useRouter } from "next/router"; - -// components -import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; -import { CustomMenu, Icon, Spinner } from "components/ui"; -// hooks -import useIssuesProperties from "hooks/use-issue-properties"; -import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; -// types -import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; -// constants -import { SPREADSHEET_COLUMN } from "constants/spreadsheet"; -// icon -import { PlusIcon } from "@heroicons/react/24/outline"; - -type Props = { - handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; - openIssuesListModal?: (() => void) | null; - disableUserActions: boolean; - user: ICurrentUserResponse | undefined; - userAuth: UserAuth; -}; - -export const SpreadsheetView: React.FC = ({ - handleIssueAction, - openIssuesListModal, - disableUserActions, - user, - userAuth, -}) => { - const [expandedIssues, setExpandedIssues] = useState([]); - - const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; - - const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; - - const { spreadsheetIssues } = useSpreadsheetIssuesView(); - - const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); - - const columnData = SPREADSHEET_COLUMN.map((column) => ({ - ...column, - isActive: properties - ? column.propertyName === "labels" - ? properties[column.propertyName as keyof Properties] - : column.propertyName === "title" - ? true - : properties[column.propertyName as keyof Properties] - : false, - })); - - const gridTemplateColumns = columnData - .filter((column) => column.isActive) - .map((column) => column.colSize) - .join(" "); - - return ( -
-
- -
- {spreadsheetIssues ? ( -
- {spreadsheetIssues.map((issue: IIssue, index) => ( - - ))} -
- {type === "issue" ? ( - - ) : ( - !disableUserActions && ( - - - Add Issue - - } - position="left" - optionsClassName="left-5 !w-36" - noBorder - > - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - Create new - - {openIssuesListModal && ( - - Add an existing issue - - )} - - ) - )} -
-
- ) : ( - - )} -
- ); -}; diff --git a/apps/app/components/gantt-chart/blocks/block.tsx b/apps/app/components/gantt-chart/blocks/block.tsx deleted file mode 100644 index 52fd1fe52fb..00000000000 --- a/apps/app/components/gantt-chart/blocks/block.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import Link from "next/link"; -import { useRouter } from "next/router"; - -// ui -import { Tooltip } from "components/ui"; -// helpers -import { renderShortDate } from "helpers/date-time.helper"; -// types -import { ICycle, IIssue, IModule } from "types"; -// constants -import { MODULE_STATUS } from "constants/module"; - -export const IssueGanttBlock = ({ issue }: { issue: IIssue }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - return ( - - -
- -
{issue.name}
-
- {renderShortDate(issue.start_date ?? "")} to{" "} - {renderShortDate(issue.target_date ?? "")} -
-
- } - position="top-left" - > -
- {issue.name} -
- -
- - ); -}; - -export const CycleGanttBlock = ({ cycle }: { cycle: ICycle }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - return ( - - -
- -
{cycle.name}
-
- {renderShortDate(cycle.start_date ?? "")} to {renderShortDate(cycle.end_date ?? "")} -
-
- } - position="top-left" - > -
- {cycle.name} -
- -
- - ); -}; - -export const ModuleGanttBlock = ({ module }: { module: IModule }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - return ( - - -
s.value === module.status)?.color }} - /> - -
{module.name}
-
- {renderShortDate(module.start_date ?? "")} to{" "} - {renderShortDate(module.target_date ?? "")} -
-
- } - position="top-left" - > -
- {module.name} -
- -
- - ); -}; diff --git a/apps/app/components/gantt-chart/blocks/blocks-display.tsx b/apps/app/components/gantt-chart/blocks/blocks-display.tsx deleted file mode 100644 index fd43c733ee3..00000000000 --- a/apps/app/components/gantt-chart/blocks/blocks-display.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { FC } from "react"; - -// react-beautiful-dnd -import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; -// helpers -import { ChartDraggable } from "../helpers/draggable"; -import { renderDateFormat } from "helpers/date-time.helper"; -// types -import { IBlockUpdateData, IGanttBlock } from "../types"; - -export const GanttChartBlocks: FC<{ - itemsContainerWidth: number; - blocks: IGanttBlock[] | null; - sidebarBlockRender: FC; - blockRender: FC; - blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - enableLeftDrag: boolean; - enableRightDrag: boolean; - enableReorder: boolean; -}> = ({ - itemsContainerWidth, - blocks, - sidebarBlockRender, - blockRender, - blockUpdateHandler, - enableLeftDrag, - enableRightDrag, - enableReorder, -}) => { - const handleChartBlockPosition = ( - block: IGanttBlock, - totalBlockShifts: number, - dragDirection: "left" | "right" - ) => { - let updatedDate = new Date(); - - if (dragDirection === "left") { - const originalDate = new Date(block.start_date); - - const currentDay = originalDate.getDate(); - updatedDate = new Date(originalDate); - - updatedDate.setDate(currentDay - totalBlockShifts); - } else { - const originalDate = new Date(block.target_date); - - const currentDay = originalDate.getDate(); - updatedDate = new Date(originalDate); - - updatedDate.setDate(currentDay + totalBlockShifts); - } - - blockUpdateHandler(block.data, { - [dragDirection === "left" ? "start_date" : "target_date"]: renderDateFormat(updatedDate), - }); - }; - - const handleOrderChange = (result: DropResult) => { - if (!blocks) return; - - const { source, destination, draggableId } = result; - - if (!destination) return; - - if (source.index === destination.index && document) { - // const draggedBlock = document.querySelector(`#${draggableId}`) as HTMLElement; - // const blockStyles = window.getComputedStyle(draggedBlock); - - // console.log(blockStyles.marginLeft); - - return; - } - - let updatedSortOrder = blocks[source.index].sort_order; - - if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; - else if (destination.index === blocks.length - 1) - updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; - else { - const destinationSortingOrder = blocks[destination.index].sort_order; - const relativeDestinationSortingOrder = - source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; - - updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; - } - - const removedElement = blocks.splice(source.index, 1)[0]; - blocks.splice(destination.index, 0, removedElement); - - blockUpdateHandler(removedElement.data, { - sort_order: { - destinationIndex: destination.index, - newSortOrder: updatedSortOrder, - sourceIndex: source.index, - }, - }); - }; - - return ( -
- - - {(droppableProvided, droppableSnapshot) => ( -
- <> - {blocks && - blocks.length > 0 && - blocks.map( - (block, index: number) => - block.start_date && - block.target_date && ( - - {(provided) => ( -
- handleChartBlockPosition(block, ...args)} - enableLeftDrag={enableLeftDrag} - enableRightDrag={enableRightDrag} - provided={provided} - > -
- {blockRender({ - ...block.data, - })} -
-
-
- )} -
- ) - )} - {droppableProvided.placeholder} - -
- )} -
-
- - {/* sidebar */} - {/*
- {blocks && - blocks.length > 0 && - blocks.map((block: any, _idx: number) => ( -
- {sidebarBlockRender(block?.data)} -
- ))} -
*/} -
- ); -}; diff --git a/apps/app/components/gantt-chart/helpers/draggable.tsx b/apps/app/components/gantt-chart/helpers/draggable.tsx deleted file mode 100644 index 320f4355fee..00000000000 --- a/apps/app/components/gantt-chart/helpers/draggable.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import React, { useRef, useState } from "react"; - -// react-beautiful-dnd -import { DraggableProvided } from "react-beautiful-dnd"; -import { useChart } from "../hooks"; -// types -import { IGanttBlock } from "../types"; - -type Props = { - children: any; - block: IGanttBlock; - handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right") => void; - enableLeftDrag: boolean; - enableRightDrag: boolean; - provided: DraggableProvided; -}; - -export const ChartDraggable: React.FC = ({ - children, - block, - handleBlock, - enableLeftDrag = true, - enableRightDrag = true, - provided, -}) => { - const [isLeftResizing, setIsLeftResizing] = useState(false); - const [isRightResizing, setIsRightResizing] = useState(false); - - const parentDivRef = useRef(null); - const resizableRef = useRef(null); - - const { currentViewData } = useChart(); - - const checkScrollEnd = (e: MouseEvent): number => { - let delWidth = 0; - - const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; - const appSidebar = document.querySelector("#app-sidebar") as HTMLElement; - - const posFromLeft = e.clientX; - // manually scroll to left if reached the left end while dragging - if (posFromLeft - appSidebar.clientWidth <= 70) { - if (e.movementX > 0) return 0; - - delWidth = -5; - - scrollContainer.scrollBy(delWidth, 0); - } else delWidth = e.movementX; - - // manually scroll to right if reached the right end while dragging - const posFromRight = window.innerWidth - e.clientX; - if (posFromRight <= 70) { - if (e.movementX < 0) return 0; - - delWidth = 5; - - scrollContainer.scrollBy(delWidth, 0); - } else delWidth = e.movementX; - - return delWidth; - }; - - const handleLeftDrag = () => { - if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position) - return; - - const resizableDiv = resizableRef.current; - const parentDiv = parentDivRef.current; - - const columnWidth = currentViewData.data.width; - - const blockInitialWidth = - resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); - - let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); - let initialMarginLeft = parseInt(parentDiv.style.marginLeft); - - const handleMouseMove = (e: MouseEvent) => { - if (!window) return; - - let delWidth = 0; - - delWidth = checkScrollEnd(e); - - // calculate new width and update the initialMarginLeft using -= - const newWidth = Math.round((initialWidth -= delWidth) / columnWidth) * columnWidth; - // calculate new marginLeft and update the initial marginLeft to the newly calculated one - const newMarginLeft = initialMarginLeft - (newWidth - (block.position?.width ?? 0)); - initialMarginLeft = newMarginLeft; - - // block needs to be at least 1 column wide - if (newWidth < columnWidth) return; - - resizableDiv.style.width = `${newWidth}px`; - parentDiv.style.marginLeft = `${newMarginLeft}px`; - - if (block.position) { - block.position.width = newWidth; - block.position.marginLeft = newMarginLeft; - } - }; - - const handleMouseUp = () => { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - - const totalBlockShifts = Math.ceil( - (resizableDiv.clientWidth - blockInitialWidth) / columnWidth - ); - - handleBlock(totalBlockShifts, "left"); - }; - - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - }; - - const handleRightDrag = () => { - if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position) - return; - - const resizableDiv = resizableRef.current; - - const columnWidth = currentViewData.data.width; - - const blockInitialWidth = - resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); - - let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); - - const handleMouseMove = (e: MouseEvent) => { - if (!window) return; - - let delWidth = 0; - - delWidth = checkScrollEnd(e); - - // calculate new width and update the initialMarginLeft using += - const newWidth = Math.round((initialWidth += delWidth) / columnWidth) * columnWidth; - - // block needs to be at least 1 column wide - if (newWidth < columnWidth) return; - - resizableDiv.style.width = `${Math.max(newWidth, 80)}px`; - if (block.position) block.position.width = Math.max(newWidth, 80); - }; - - const handleMouseUp = () => { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - - const totalBlockShifts = Math.ceil( - (resizableDiv.clientWidth - blockInitialWidth) / columnWidth - ); - - handleBlock(totalBlockShifts, "right"); - }; - - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - }; - - return ( -
- {enableLeftDrag && ( - <> -
setIsLeftResizing(true)} - onMouseLeave={() => setIsLeftResizing(false)} - className="absolute top-1/2 -left-2.5 -translate-y-1/2 z-[1] w-6 h-10 bg-brand-backdrop rounded-md cursor-col-resize" - /> -
- - )} - {React.cloneElement(children, { ref: resizableRef, ...provided.dragHandleProps })} - {enableRightDrag && ( - <> -
setIsRightResizing(true)} - onMouseLeave={() => setIsRightResizing(false)} - className="absolute top-1/2 -right-2.5 -translate-y-1/2 z-[1] w-6 h-6 bg-brand-backdrop rounded-md cursor-col-resize" - /> -
- - )} -
- ); -}; diff --git a/apps/app/components/icons/blocked-icon.tsx b/apps/app/components/icons/blocked-icon.tsx deleted file mode 100644 index ee0024fa093..00000000000 --- 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 093728cd8e5..00000000000 --- 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 569767aa52c..00000000000 --- 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 c3170ca329d..00000000000 --- 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/clipboard-icon.tsx b/apps/app/components/icons/clipboard-icon.tsx deleted file mode 100644 index c96aa3fde30..00000000000 --- 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 c60cca4a609..00000000000 --- 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 615fbcb9a9e..00000000000 --- 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/current-cycle-icon.tsx b/apps/app/components/icons/current-cycle-icon.tsx deleted file mode 100644 index 2b07edf2e6d..00000000000 --- 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 c4e012e4dfe..00000000000 --- 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 cfdd66751e7..00000000000 --- 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 d0c9cffb74b..00000000000 --- 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 0a8816b751b..00000000000 --- 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 0b958a21d67..00000000000 --- 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/question-mark-circle-icon.tsx b/apps/app/components/icons/question-mark-circle-icon.tsx deleted file mode 100644 index 2cdf9d8e575..00000000000 --- 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 0e785d95867..00000000000 --- 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/tag-icon.tsx b/apps/app/components/icons/tag-icon.tsx deleted file mode 100644 index a17d4c1e400..00000000000 --- 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 176c38b2908..00000000000 --- 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 1221b297679..00000000000 --- 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/upcoming-cycle-icon.tsx b/apps/app/components/icons/upcoming-cycle-icon.tsx deleted file mode 100644 index 52961e15ea8..00000000000 --- 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 8bae3413367..00000000000 --- 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 c0408dad36e..00000000000 --- 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 3bd1c965c64..00000000000 --- 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/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx deleted file mode 100644 index 9d00bf784a4..00000000000 --- a/apps/app/components/issues/comment/add-comment.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// react-hook-form -import { useForm, Controller } from "react-hook-form"; -// services -import issuesServices from "services/issues.service"; -// hooks -import useToast from "hooks/use-toast"; -// ui -import { SecondaryButton } from "components/ui"; -// types -import type { ICurrentUserResponse, IIssueComment } from "types"; -// fetch-keys -import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; -import Tiptap, { ITiptapRichTextEditor } from "components/tiptap"; - -const TiptapEditor = React.forwardRef( - (props, ref) => -); - -TiptapEditor.displayName = "TiptapEditor"; - -const defaultValues: Partial = { - comment_json: "", - comment_html: "", -}; - -type Props = { - issueId: string; - user: ICurrentUserResponse | undefined; - disabled?: boolean; -}; - -export const AddComment: React.FC = ({ issueId, user, disabled = false }) => { - const { - handleSubmit, - control, - setValue, - watch, - formState: { isSubmitting }, - reset, - } = useForm({ defaultValues }); - - const editorRef = React.useRef(null); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { setToastAlert } = useToast(); - - const onSubmit = async (formData: IIssueComment) => { - if ( - !workspaceSlug || - !projectId || - !issueId || - isSubmitting || - !formData.comment_html || - !formData.comment_json - ) - return; - await issuesServices - .createIssueComment( - workspaceSlug as string, - projectId as string, - issueId as string, - formData, - user - ) - .then(() => { - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - reset(defaultValues); - editorRef.current?.clearEditor(); - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Comment could not be posted. Please try again.", - }) - ); - }; - - return ( -
-
-
- ( - { - onChange(comment_html); - setValue("comment_json", comment_json); - }} - /> - )} - /> - - - {isSubmitting ? "Adding..." : "Comment"} - -
-
-
- ); -}; diff --git a/apps/app/components/issues/sidebar-select/assignee.tsx b/apps/app/components/issues/sidebar-select/assignee.tsx deleted file mode 100644 index 323d201e8eb..00000000000 --- a/apps/app/components/issues/sidebar-select/assignee.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// services -import projectService from "services/project.service"; -// ui -import { CustomSearchSelect } from "components/ui"; -import { AssigneesList, Avatar } from "components/ui/avatar"; -// icons -import { UserGroupIcon } from "@heroicons/react/24/outline"; -// types -import { UserAuth } from "types"; -// fetch-keys -import { PROJECT_MEMBERS } from "constants/fetch-keys"; - -type Props = { - value: string[]; - onChange: (val: string[]) => void; - userAuth: UserAuth; - disabled?: boolean; -}; - -export const SidebarAssigneeSelect: React.FC = ({ - value, - onChange, - userAuth, - disabled = false, -}) => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { data: members } = useSWR( - workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) - : null - ); - - const options = members?.map((member) => ({ - value: member.member.id, - query: member.member.display_name, - content: ( -
- - {member.member.display_name} -
- ), - })); - - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - - return ( -
-
- -

Assignees

-
-
- - {value && value.length > 0 && Array.isArray(value) ? ( -
- - {value.length} Assignees -
- ) : ( - "No assignees" - )} -
- } - options={options} - onChange={onChange} - position="right" - multiple - disabled={isNotAllowed} - /> -
-
- ); -}; diff --git a/apps/app/components/issues/sidebar-select/cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx deleted file mode 100644 index 1eacba2451e..00000000000 --- a/apps/app/components/issues/sidebar-select/cycle.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -import useSWR, { mutate } from "swr"; - -// services -import issuesService from "services/issues.service"; -import cyclesService from "services/cycles.service"; -// ui -import { Spinner, CustomSelect, Tooltip } from "components/ui"; -// helper -import { truncateText } from "helpers/string.helper"; -// icons -import { ContrastIcon } from "components/icons"; -// types -import { ICycle, IIssue, UserAuth } from "types"; -// fetch-keys -import { CYCLE_ISSUES, INCOMPLETE_CYCLES_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; - -type Props = { - issueDetail: IIssue | undefined; - handleCycleChange: (cycle: ICycle) => void; - userAuth: UserAuth; - disabled?: boolean; -}; - -export const SidebarCycleSelect: React.FC = ({ - issueDetail, - handleCycleChange, - userAuth, - disabled = false, -}) => { - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { data: incompleteCycles } = useSWR( - workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => - cyclesService.getCyclesWithParams( - workspaceSlug as string, - projectId as string, - "incomplete" - ) - : null - ); - - const removeIssueFromCycle = (bridgeId: string, cycleId: string) => { - if (!workspaceSlug || !projectId) return; - - issuesService - .removeIssueFromCycle(workspaceSlug as string, projectId as string, cycleId, bridgeId) - .then((res) => { - mutate(ISSUE_DETAILS(issueId as string)); - - mutate(CYCLE_ISSUES(cycleId)); - }) - .catch((e) => { - console.log(e); - }); - }; - - const issueCycle = issueDetail?.issue_cycle; - - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - - return ( -
-
- -

Cycle

-
-
- - - - {issueCycle ? truncateText(issueCycle.cycle_detail.name, 15) : "No cycle"} - - - - } - value={issueCycle ? issueCycle.cycle_detail.id : null} - onChange={(value: any) => { - !value - ? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "") - : handleCycleChange(incompleteCycles?.find((c) => c.id === value) as ICycle); - }} - width="w-full" - position="right" - maxHeight="rg" - disabled={isNotAllowed} - > - {incompleteCycles ? ( - incompleteCycles.length > 0 ? ( - <> - {incompleteCycles.map((option) => ( - - - {truncateText(option.name, 25)} - - - ))} - None - - ) : ( -
No cycles found
- ) - ) : ( - - )} -
-
-
- ); -}; diff --git a/apps/app/components/issues/sidebar-select/estimate.tsx b/apps/app/components/issues/sidebar-select/estimate.tsx deleted file mode 100644 index ab8bfac1541..00000000000 --- a/apps/app/components/issues/sidebar-select/estimate.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from "react"; - -// hooks -import useEstimateOption from "hooks/use-estimate-option"; -// ui -import { CustomSelect } from "components/ui"; -// icons -import { PlayIcon } from "@heroicons/react/24/outline"; -// types -import { UserAuth } from "types"; - -type Props = { - value: number | null; - onChange: (val: number | null) => void; - userAuth: UserAuth; - disabled?: boolean; -}; - -export const SidebarEstimateSelect: React.FC = ({ - value, - onChange, - userAuth, - disabled = false, -}) => { - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - - const { isEstimateActive, estimatePoints } = useEstimateOption(); - - if (!isEstimateActive) return null; - - return ( -
-
- -

Estimate

-
-
- - - {estimatePoints?.find((e) => e.key === value)?.value ?? ( - No estimates - )} -
- } - onChange={onChange} - position="right" - width="w-full" - disabled={isNotAllowed || disabled} - > - - <> - - - - None - - - {estimatePoints && - estimatePoints.map((point) => ( - - <> - - - - {point.value} - - - ))} - -
-
- ); -}; diff --git a/apps/app/components/issues/sidebar-select/module.tsx b/apps/app/components/issues/sidebar-select/module.tsx deleted file mode 100644 index a8770e03dff..00000000000 --- a/apps/app/components/issues/sidebar-select/module.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -import useSWR, { mutate } from "swr"; - -// services -import modulesService from "services/modules.service"; -// ui -import { Spinner, CustomSelect, Tooltip } from "components/ui"; -// helper -import { truncateText } from "helpers/string.helper"; -// icons -import { RectangleGroupIcon } from "@heroicons/react/24/outline"; -// types -import { IIssue, IModule, UserAuth } from "types"; -// fetch-keys -import { ISSUE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys"; - -type Props = { - issueDetail: IIssue | undefined; - handleModuleChange: (module: IModule) => void; - userAuth: UserAuth; - disabled?: boolean; -}; - -export const SidebarModuleSelect: React.FC = ({ - issueDetail, - handleModuleChange, - userAuth, - disabled = false, -}) => { - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { data: modules } = useSWR( - workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => modulesService.getModules(workspaceSlug as string, projectId as string) - : null - ); - - const removeIssueFromModule = (bridgeId: string, moduleId: string) => { - if (!workspaceSlug || !projectId) return; - - modulesService - .removeIssueFromModule(workspaceSlug as string, projectId as string, moduleId, bridgeId) - .then((res) => { - mutate(ISSUE_DETAILS(issueId as string)); - - mutate(MODULE_ISSUES(moduleId)); - }) - .catch((e) => { - console.log(e); - }); - }; - - const issueModule = issueDetail?.issue_module; - - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - - return ( -
-
- -

Module

-
-
- m.id === issueModule?.module)?.name ?? "No module" - }`} - > - - - {truncateText( - `${modules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"}`, - 15 - )} - - - - } - value={issueModule ? issueModule.module_detail?.id : null} - onChange={(value: any) => { - !value - ? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "") - : handleModuleChange(modules?.find((m) => m.id === value) as IModule); - }} - width="w-full" - position="right" - maxHeight="rg" - disabled={isNotAllowed} - > - {modules ? ( - modules.length > 0 ? ( - <> - {modules.map((option) => ( - - - {truncateText(option.name, 25)} - - - ))} - None - - ) : ( -
No modules found
- ) - ) : ( - - )} -
-
-
- ); -}; diff --git a/apps/app/components/issues/sidebar-select/parent.tsx b/apps/app/components/issues/sidebar-select/parent.tsx deleted file mode 100644 index 1e780dd5725..00000000000 --- a/apps/app/components/issues/sidebar-select/parent.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useState } from "react"; - -import { useRouter } from "next/router"; - -// icons -import { UserIcon } from "@heroicons/react/24/outline"; -// components -import { ParentIssuesListModal } from "components/issues"; -// types -import { IIssue, ISearchIssueResponse, UserAuth } from "types"; - -type Props = { - onChange: (value: string) => void; - issueDetails: IIssue | undefined; - userAuth: UserAuth; - disabled?: boolean; -}; - -export const SidebarParentSelect: React.FC = ({ - onChange, - issueDetails, - userAuth, - disabled = false, -}) => { - const [isParentModalOpen, setIsParentModalOpen] = useState(false); - const [selectedParentIssue, setSelectedParentIssue] = useState(null); - - const router = useRouter(); - const { projectId, issueId } = router.query; - - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - - return ( -
-
- -

Parent

-
-
- setIsParentModalOpen(false)} - onChange={(issue) => { - onChange(issue.id); - setSelectedParentIssue(issue); - }} - issueId={issueId as string} - projectId={projectId as string} - /> - -
-
- ); -}; diff --git a/apps/app/components/issues/sidebar-select/priority.tsx b/apps/app/components/issues/sidebar-select/priority.tsx deleted file mode 100644 index a97fe06fbbf..00000000000 --- a/apps/app/components/issues/sidebar-select/priority.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from "react"; - -// ui -import { CustomSelect } from "components/ui"; -// icons -import { ChartBarIcon } from "@heroicons/react/24/outline"; -import { getPriorityIcon } from "components/icons/priority-icon"; -// types -import { UserAuth } from "types"; -// constants -import { PRIORITIES } from "constants/project"; - -type Props = { - value: string | null; - onChange: (val: string) => void; - userAuth: UserAuth; - disabled?: boolean; -}; - -export const SidebarPrioritySelect: React.FC = ({ - value, - onChange, - userAuth, - disabled = false, -}) => { - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - - return ( -
-
- -

Priority

-
-
- - - {getPriorityIcon(value ?? "None", "text-sm")} - - - {value ?? "None"} - -
- } - value={value} - onChange={onChange} - width="w-full" - position="right" - disabled={isNotAllowed} - > - {PRIORITIES.map((option) => ( - - <> - {getPriorityIcon(option, "text-sm")} - {option ?? "None"} - - - ))} - -
-
- ); -}; diff --git a/apps/app/components/issues/sidebar-select/state.tsx b/apps/app/components/issues/sidebar-select/state.tsx deleted file mode 100644 index b1751b070c0..00000000000 --- a/apps/app/components/issues/sidebar-select/state.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// services -import stateService from "services/state.service"; -// ui -import { Spinner, CustomSelect } from "components/ui"; -// icons -import { Squares2X2Icon } from "@heroicons/react/24/outline"; -import { getStateGroupIcon } from "components/icons"; -// helpers -import { getStatesList } from "helpers/state.helper"; -import { addSpaceIfCamelCase } from "helpers/string.helper"; -// types -import { UserAuth } from "types"; -// constants -import { STATES_LIST } from "constants/fetch-keys"; - -type Props = { - value: string; - onChange: (val: string) => void; - userAuth: UserAuth; - disabled?: boolean; -}; - -export const SidebarStateSelect: React.FC = ({ - value, - onChange, - userAuth, - disabled = false, -}) => { - const router = useRouter(); - const { workspaceSlug, projectId, inboxIssueId } = router.query; - - const { data: stateGroups } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => stateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - const states = getStatesList(stateGroups); - - const selectedState = states?.find((s) => s.id === value); - - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - - return ( -
-
- -

State

-
-
- - {getStateGroupIcon( - selectedState?.group ?? "backlog", - "16", - "16", - selectedState?.color ?? "" - )} - {addSpaceIfCamelCase(selectedState?.name ?? "")} -
- ) : inboxIssueId ? ( -
- {getStateGroupIcon("backlog", "16", "16", "#ff7700")} - Triage -
- ) : ( - "None" - ) - } - value={value} - onChange={onChange} - width="w-full" - position="right" - disabled={isNotAllowed} - > - {states ? ( - states.length > 0 ? ( - states.map((state) => ( - - <> - {getStateGroupIcon(state.group, "16", "16", state.color)} - {state.name} - - - )) - ) : ( -
No states found
- ) - ) : ( - - )} - -
-
- ); -}; diff --git a/apps/app/components/project/publish-project/modal.tsx b/apps/app/components/project/publish-project/modal.tsx deleted file mode 100644 index 5f9d9ae2cbd..00000000000 --- a/apps/app/components/project/publish-project/modal.tsx +++ /dev/null @@ -1,474 +0,0 @@ -import React, { useEffect } from "react"; -// next imports -import { useRouter } from "next/router"; -// react-hook-form -import { useForm } from "react-hook-form"; -// headless ui -import { Dialog, Transition } from "@headlessui/react"; -// ui components -import { ToggleSwitch, PrimaryButton, SecondaryButton } from "components/ui"; -import { CustomPopover } from "./popover"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; -import { IProjectPublishSettingsViews } from "store/project-publish"; -// hooks -import useToast from "hooks/use-toast"; -import useProjectDetails from "hooks/use-project-details"; - -type Props = { - // user: ICurrentUserResponse | undefined; -}; - -const defaultValues: Partial = { - id: null, - comments: false, - reactions: false, - votes: false, - inbox: null, - views: ["list", "kanban"], -}; - -const viewOptions = [ - { key: "list", value: "List" }, - { key: "kanban", value: "Kanban" }, - // { key: "calendar", value: "Calendar" }, - // { key: "gantt", value: "Gantt" }, - // { key: "spreadsheet", value: "Spreadsheet" }, -]; - -export const PublishProjectModal: React.FC = observer(() => { - const store: RootStore = useMobxStore(); - const { projectPublish } = store; - - const { projectDetails, mutateProjectDetails } = useProjectDetails(); - - const { setToastAlert } = useToast(); - const handleToastAlert = (title: string, type: string, message: string) => { - setToastAlert({ - title: title || "Title", - type: "error" || "warning", - message: message || "Message", - }); - }; - - const { NEXT_PUBLIC_DEPLOY_URL } = process.env; - const plane_deploy_url = NEXT_PUBLIC_DEPLOY_URL - ? NEXT_PUBLIC_DEPLOY_URL - : "http://localhost:3001"; - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { - formState: { errors, isSubmitting }, - handleSubmit, - reset, - watch, - setValue, - } = useForm({ - defaultValues, - reValidateMode: "onChange", - }); - - const handleClose = () => { - projectPublish.handleProjectModal(null); - reset({ ...defaultValues }); - }; - - useEffect(() => { - if ( - projectPublish.projectPublishSettings && - projectPublish.projectPublishSettings != "not-initialized" - ) { - let userBoards: string[] = []; - if (projectPublish.projectPublishSettings?.views) { - const _views: IProjectPublishSettingsViews | null = - projectPublish.projectPublishSettings?.views || null; - if (_views != null) { - if (_views.list) userBoards.push("list"); - if (_views.kanban) userBoards.push("kanban"); - if (_views.calendar) userBoards.push("calendar"); - if (_views.gantt) userBoards.push("gantt"); - if (_views.spreadsheet) userBoards.push("spreadsheet"); - userBoards = userBoards && userBoards.length > 0 ? userBoards : ["list"]; - } - } - - const updatedData = { - id: projectPublish.projectPublishSettings?.id || null, - comments: projectPublish.projectPublishSettings?.comments || false, - reactions: projectPublish.projectPublishSettings?.reactions || false, - votes: projectPublish.projectPublishSettings?.votes || false, - inbox: projectPublish.projectPublishSettings?.inbox || null, - views: userBoards, - }; - reset({ ...updatedData }); - } - }, [reset, projectPublish.projectPublishSettings]); - - useEffect(() => { - if ( - projectPublish.projectPublishModal && - workspaceSlug && - projectPublish.project_id != null && - projectPublish?.projectPublishSettings === "not-initialized" - ) { - projectPublish.getProjectSettingsAsync( - workspaceSlug as string, - projectPublish.project_id as string, - null - ); - } - }, [workspaceSlug, projectPublish, projectPublish.projectPublishModal]); - - const onSettingsPublish = async (formData: any) => { - if (formData.views && formData.views.length > 0) { - const payload = { - comments: formData.comments || false, - reactions: formData.reactions || false, - votes: formData.votes || false, - inbox: formData.inbox || null, - views: { - list: formData.views.includes("list") || false, - kanban: formData.views.includes("kanban") || false, - calendar: formData.views.includes("calendar") || false, - gantt: formData.views.includes("gantt") || false, - spreadsheet: formData.views.includes("spreadsheet") || false, - }, - }; - - const _workspaceSlug = workspaceSlug; - const _projectId = projectPublish.project_id; - - return projectPublish - .createProjectSettingsAsync(_workspaceSlug as string, _projectId as string, payload, null) - .then((response) => { - mutateProjectDetails(); - handleClose(); - console.log("_projectId", _projectId); - if (_projectId) - window.open(`${plane_deploy_url}/${_workspaceSlug}/${_projectId}`, "_blank"); - return response; - }) - .catch((error) => { - console.error("error", error); - return error; - }); - } else { - handleToastAlert("Missing fields", "warning", "Please select at least one view to publish"); - } - }; - - const onSettingsUpdate = async (key: string, value: any) => { - const payload = { - comments: key === "comments" ? value : watch("comments"), - reactions: key === "reactions" ? value : watch("reactions"), - votes: key === "votes" ? value : watch("votes"), - inbox: key === "inbox" ? value : watch("inbox"), - views: - key === "views" - ? { - list: value.includes("list") ? true : false, - kanban: value.includes("kanban") ? true : false, - calendar: value.includes("calendar") ? true : false, - gantt: value.includes("gantt") ? true : false, - spreadsheet: value.includes("spreadsheet") ? true : false, - } - : { - list: watch("views").includes("list") ? true : false, - kanban: watch("views").includes("kanban") ? true : false, - calendar: watch("views").includes("calendar") ? true : false, - gantt: watch("views").includes("gantt") ? true : false, - spreadsheet: watch("views").includes("spreadsheet") ? true : false, - }, - }; - - return projectPublish - .updateProjectSettingsAsync( - workspaceSlug as string, - projectPublish.project_id as string, - watch("id"), - payload, - null - ) - .then((response) => { - mutateProjectDetails(); - return response; - }) - .catch((error) => { - console.log("error", error); - return error; - }); - }; - - const onSettingsUnPublish = async (formData: any) => - projectPublish - .deleteProjectSettingsAsync( - workspaceSlug as string, - projectPublish.project_id as string, - formData?.id, - null - ) - .then((response) => { - mutateProjectDetails(); - reset({ ...defaultValues }); - handleClose(); - return response; - }) - .catch((error) => { - console.error("error", error); - return error; - }); - - const CopyLinkToClipboard = ({ copy_link }: { copy_link: string }) => { - const [status, setStatus] = React.useState(false); - - const copyText = () => { - navigator.clipboard.writeText(copy_link); - setStatus(true); - setTimeout(() => { - setStatus(false); - }, 1000); - }; - - return ( -
copyText()} - > - {status ? "Copied" : "Copy Link"} -
- ); - }; - - return ( - - - -
- - -
-
- - - {/* heading */} -
-
Publish
- {projectPublish.loader && ( -
Changes saved
- )} -
- close -
-
- - {/* content */} -
- {watch("id") && ( -
-
- - radio_button_checked - -
-
This project is live on web
-
- )} - -
-
- {`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`} -
- -
- -
-
-
Views
-
- 0 - ? viewOptions - .filter( - (_view) => watch("views").includes(_view.key) && _view.value - ) - .map((_view) => _view.value) - .join(", ") - : `` - } - placeholder="Select views" - > - <> - {viewOptions && - viewOptions.length > 0 && - viewOptions.map((_view) => ( -
{ - const _views = - watch("views") && watch("views").length > 0 - ? watch("views").includes(_view?.key) - ? watch("views").filter((_o: string) => _o !== _view?.key) - : [...watch("views"), _view?.key] - : [_view?.key]; - setValue("views", _views); - if (watch("id") != null) onSettingsUpdate("views", _views); - }} - > -
{_view.value}
-
- {watch("views") && - watch("views").length > 0 && - watch("views").includes(_view.key) && ( - - done - - )} -
-
- ))} - -
-
-
- - {/*
-
Allow comments
-
- { - const _comments = !watch("comments"); - setValue("comments", _comments); - if (watch("id") != null) onSettingsUpdate("comments", _comments); - }} - size="sm" - /> -
-
*/} - - {/*
-
Allow reactions
-
- { - const _reactions = !watch("reactions"); - setValue("reactions", _reactions); - if (watch("id") != null) onSettingsUpdate("reactions", _reactions); - }} - size="sm" - /> -
-
*/} - - {/*
-
Allow Voting
-
- { - const _votes = !watch("votes"); - setValue("votes", _votes); - if (watch("id") != null) onSettingsUpdate("votes", _votes); - }} - size="sm" - /> -
-
*/} - - {/*
-
Allow issue proposals
-
- { - setValue("inbox", !watch("inbox")); - }} - size="sm" - /> -
-
*/} -
-
- - {/* modal handlers */} -
-
-
- public -
-
Anyone with the link can access
-
-
- Cancel - {watch("id") != null ? ( - - {isSubmitting ? "Unpublishing..." : "Unpublish"} - - ) : ( - - {isSubmitting ? "Publishing..." : "Publish"} - - )} -
-
-
-
-
-
-
-
- ); -}); 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 7bfca0d2c4f..00000000000 --- a/apps/app/components/project/single-sidebar-project.tsx +++ /dev/null @@ -1,357 +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 { truncateText } from "helpers/string.helper"; -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)} - > -
-
- ios_share -
-
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/workspace/issues-stats.tsx b/apps/app/components/workspace/issues-stats.tsx deleted file mode 100644 index 1b327227ac2..00000000000 --- 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/app/constants/module.ts b/apps/app/constants/module.ts deleted file mode 100644 index ffacdfa3c2c..00000000000 --- a/apps/app/constants/module.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const MODULE_STATUS = [ - { label: "Backlog", value: "backlog", color: "#5e6ad2" }, - { label: "Planned", value: "planned", color: "#26b5ce" }, - { label: "In Progress", value: "in-progress", color: "#f2c94c" }, - { label: "Paused", value: "paused", color: "#ff6900" }, - { label: "Completed", value: "completed", color: "#4cb782" }, - { label: "Cancelled", value: "cancelled", color: "#cc1d10" }, -]; diff --git a/apps/space/.env.example b/apps/space/.env.example deleted file mode 100644 index 4fb0e4df6c9..00000000000 --- 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 7b4ed614233..00000000000 --- 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 81c2b48c2c1..00000000000 --- 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 b63f748e8ab..00000000000 --- 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 6a18b728395..00000000000 --- 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 5f23e0f3aa0..00000000000 --- 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 6d3cc3cc034..00000000000 --- 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 90cc1629c93..00000000000 --- 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 87cd65938da..00000000000 --- 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 22af77568b9..00000000000 --- 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 b9dfcc6ab77..00000000000 --- 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 7a7ec0de13a..00000000000 --- 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 e01d0ae5899..00000000000 --- 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 366ae103010..00000000000 --- 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 0606bfc9524..00000000000 --- 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 7d313153a78..00000000000 --- 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 98173fd66d6..00000000000 --- 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 2253a0be22d..00000000000 --- 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 95a4f4c7094..00000000000 --- 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 fc73af381f2..00000000000 --- 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 71a2fabcc40..00000000000 --- 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 57c8b27c16f..00000000000 --- 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 10255882d6e..00000000000 --- 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 c122f847841..00000000000 --- 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 fd4dba21770..00000000000 --- 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 a31bc822fde..00000000000 --- 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 4128b636a7d..00000000000 --- 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 e37dfa54a76..00000000000 --- 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 8681006e165..00000000000 --- 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 52e7c98da21..00000000000 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 38b2f7a1d37..00000000000 --- 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 79ad4b9102c..00000000000 --- 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 809d56b97c7..00000000000 --- 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 5a0a51eda44..00000000000 --- 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 5feeba7bd40..00000000000 --- 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 0293c538168..00000000000 --- 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 2f4782236dc..00000000000 --- 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 e493f0abc1c..00000000000 --- 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 55aaa9a319a..00000000000 --- 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 5404bd9fbf9..00000000000 --- 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 fcb93c530dd..56dbbe670d4 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 diff --git a/docker-compose.yml b/docker-compose.yml index f69f5be1dde..e51f88c5568 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,176 +1,204 @@ 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 + ports: + - 8000:8000 + 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 206c94b51c3..974f4907d6c 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 804fb7b649b..eb6a23994b1 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,18 @@ "license": "AGPL-3.0", "private": true, "workspaces": [ - "apps/*", + "web", + "space", "packages/*" ], "scripts": { + "prepare": "husky install", "build": "turbo run build", "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 4d829512596..d31a7640602 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 00000000000..238f70854ef --- /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 2b28e1fd138..963dad136e4 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 NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX + +RUN yarn turbo run build --filter=space -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 +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 00000000000..b760ccfbb2a --- /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 00000000000..23742eefe15 --- /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 00000000000..c850b305cec --- /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 00000000000..e9b30ab73ed --- /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 00000000000..82916d7b569 --- /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 ( + <> +