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

- ## 📚Documentation For full documentation, visit [docs.plane.so](https://docs.plane.so/) diff --git a/apiserver/Procfile b/apiserver/Procfile index 694c49df4..63736e8e8 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 5855f0413..2dc910caf 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -20,6 +20,7 @@ from .project import ( ProjectMemberLiteSerializer, ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, + ProjectPublicMemberSerializer ) from .state import StateSerializer, StateLiteSerializer from .view import IssueViewSerializer, IssueViewFavoriteSerializer @@ -44,6 +45,7 @@ from .issue import ( IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssuePublicSerializer, ) from .module import ( diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 64ee2b8f7..938c7cab4 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -113,7 +113,11 @@ class IssueCreateSerializer(BaseSerializer): ] 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 IssueAttachmentSerializer(BaseSerializer): class IssueReactionSerializer(BaseSerializer): + + actor_detail = UserLiteSerializer(read_only=True, source="actor") + class Meta: model = IssueReaction fields = "__all__" @@ -521,19 +528,6 @@ class IssueReactionSerializer(BaseSerializer): ] -class IssueReactionLiteSerializer(BaseSerializer): - actor_detail = UserLiteSerializer(read_only=True, source="actor") - - class Meta: - model = IssueReaction - fields = [ - "id", - "reaction", - "issue", - "actor_detail", - ] - - class CommentReactionLiteSerializer(BaseSerializer): actor_detail = UserLiteSerializer(read_only=True, source="actor") @@ -554,12 +548,13 @@ class CommentReactionSerializer(BaseSerializer): 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 IssueCommentSerializer(BaseSerializer): "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 IssueLiteSerializer(BaseSerializer): ] +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 1f7a973c1..49d986cae 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -15,6 +15,7 @@ from plane.db.models import ( ProjectIdentifier, ProjectFavorite, ProjectDeployBoard, + ProjectPublicMember, ) @@ -112,7 +113,7 @@ class ProjectDetailSerializer(BaseSerializer): 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 ProjectDeployBoardSerializer(BaseSerializer): 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 b8743476e..558b7f059 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -51,6 +51,7 @@ from plane.api.views import ( WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + LeaveWorkspaceEndpoint, ## End Workspaces # File Assets FileAssetEndpoint, @@ -68,6 +69,7 @@ from plane.api.views import ( UserProjectInvitationsViewset, ProjectIdentifierEndpoint, ProjectFavoritesViewSet, + LeaveProjectEndpoint, ## End Projects # Issues IssueViewSet, @@ -89,7 +91,6 @@ from plane.api.views import ( IssueCommentPublicViewSet, IssueReactionViewSet, CommentReactionViewSet, - ExportIssuesEndpoint, ## End Issues # States StateViewSet, @@ -165,16 +166,23 @@ from plane.api.views import ( # 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 @@ urlpatterns = [ 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 @@ urlpatterns = [ WorkspaceLabelsEndpoint.as_view(), name="workspace-labels", ), + path( + "workspaces//members/leave/", + LeaveWorkspaceEndpoint.as_view(), + name="workspace-labels", + ), ## End Workspaces ## # Projects path( @@ -548,6 +561,11 @@ urlpatterns = [ ), name="project", ), + path( + "workspaces//projects//members/leave/", + LeaveProjectEndpoint.as_view(), + name="project", + ), # End Projects # States path( @@ -1490,6 +1508,15 @@ urlpatterns = [ 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 @@ urlpatterns = [ ), 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 @@ urlpatterns = [ ), 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 11223f90a..71647bfea 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -12,10 +12,11 @@ from .project import ( ProjectUserViewsEndpoint, ProjectMemberUserEndpoint, ProjectFavoritesViewSet, - ProjectDeployBoardIssuesPublicEndpoint, ProjectDeployBoardViewSet, ProjectDeployBoardPublicSettingsEndpoint, ProjectMemberEndpoint, + WorkspaceProjectDeployBoardEndpoint, + LeaveProjectEndpoint, ) from .user import ( UserEndpoint, @@ -52,6 +53,7 @@ from .workspace import ( WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, WorkspaceMembersEndpoint, + LeaveWorkspaceEndpoint, ) from .state import StateViewSet from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet @@ -84,6 +86,8 @@ from .issue import ( IssueReactionPublicViewSet, CommentReactionPublicViewSet, IssueVotePublicViewSet, + IssueRetrievePublicEndpoint, + ProjectIssuesPublicEndpoint, ) from .auth_extended import ( @@ -161,7 +165,7 @@ from .analytic import ( 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 0b935a4d3..d9b6e502d 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 3c260e03b..60b0ec0c6 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 @@ class BaseViewSet(ModelViewSet, BasePaginator): 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 a3d89fa81..253da2c5b 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -191,11 +191,10 @@ class CycleViewSet(BaseViewSet): workspace__slug=slug, project_id=project_id, ) - .annotate(first_name=F("assignees__first_name")) - .annotate(last_name=F("assignees__last_name")) + .annotate(display_name=F("assignees__display_name")) .annotate(assignee_id=F("assignees__id")) .annotate(avatar=F("assignees__avatar")) - .values("first_name", "last_name", "assignee_id", "avatar") + .values("display_name", "assignee_id", "avatar") .annotate(total_issues=Count("assignee_id")) .annotate( completed_issues=Count( @@ -209,7 +208,7 @@ class CycleViewSet(BaseViewSet): filter=Q(completed_at__isnull=True), ) ) - .order_by("first_name", "last_name") + .order_by("display_name") ) label_distribution = ( @@ -334,13 +333,21 @@ class CycleViewSet(BaseViewSet): workspace__slug=slug, project_id=project_id, pk=pk ) + request_data = request.data + if cycle.end_date is not None and cycle.end_date < timezone.now().date(): - return Response( - { - "error": "The Cycle has already been completed so it cannot be edited" - }, - status=status.HTTP_400_BAD_REQUEST, - ) + if "sort_order" in request_data: + # Can only change sort order + request_data = { + "sort_order": request_data.get("sort_order", cycle.sort_order) + } + else: + return Response( + { + "error": "The Cycle has already been completed so it cannot be edited" + }, + status=status.HTTP_400_BAD_REQUEST, + ) serializer = CycleWriteSerializer(cycle, data=request.data, partial=True) if serializer.is_valid(): @@ -374,7 +381,9 @@ class CycleViewSet(BaseViewSet): .annotate(assignee_id=F("assignees__id")) .annotate(avatar=F("assignees__avatar")) .annotate(display_name=F("assignees__display_name")) - .values("first_name", "last_name", "assignee_id", "avatar", "display_name") + .values( + "first_name", "last_name", "assignee_id", "avatar", "display_name" + ) .annotate(total_issues=Count("assignee_id")) .annotate( completed_issues=Count( @@ -710,7 +719,6 @@ class CycleDateCheckEndpoint(BaseAPIView): class CycleFavoriteViewSet(BaseViewSet): - serializer_class = CycleFavoriteSerializer model = CycleFavorite diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 6f0f1e6ae..a390f7b81 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -17,17 +17,19 @@ from django.db.models import ( When, Exists, Max, + IntegerField, ) from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from django.db.models.functions import Coalesce +from django.db import IntegrityError from django.conf import settings # 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 @@ from plane.api.serializers import ( IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssuePublicSerializer, ) from plane.api.permissions import ( WorkspaceEntityPermission, @@ -73,10 +76,12 @@ from plane.db.models import ( 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 @@ class UserWorkSpaceIssues(BaseAPIView): 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 @@ class IssueActivityEndpoint(BaseAPIView): 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 @@ class IssueActivityEndpoint(BaseAPIView): .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 @@ class IssueCommentViewSet(BaseViewSet): .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 @@ class SubIssuesEndpoint(BaseAPIView): .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 @@ class IssueReactionViewSet(BaseViewSet): 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 @@ class IssueReactionViewSet(BaseViewSet): 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 @@ class CommentReactionViewSet(BaseViewSet): 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 @@ class CommentReactionViewSet(BaseViewSet): 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 @@ class IssueCommentPublicViewSet(BaseViewSet): 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 @@ class IssueCommentPublicViewSet(BaseViewSet): 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 @@ class IssueCommentPublicViewSet(BaseViewSet): 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() + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), ) - else: + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .order_by("-created_at") + .distinct() + ) + else: + return IssueReaction.objects.none() + except ProjectDeployBoard.DoesNotExist: return IssueReaction.objects.none() def create(self, request, slug, project_id, issue_id): @@ -1648,6 +1748,23 @@ class IssueReactionPublicViewSet(BaseViewSet): 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 @@ class IssueReactionPublicViewSet(BaseViewSet): 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() + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), ) - else: + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(comment_id=self.kwargs.get("comment_id")) + .order_by("-created_at") + .distinct() + ) + else: + return CommentReaction.objects.none() + except ProjectDeployBoard.DoesNotExist: return CommentReaction.objects.none() def create(self, request, slug, project_id, comment_id): @@ -1733,8 +1866,29 @@ class CommentReactionPublicViewSet(BaseViewSet): 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 @@ class CommentReactionPublicViewSet(BaseViewSet): 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 @@ class IssueVotePublicViewSet(BaseViewSet): 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 @@ class IssueVotePublicViewSet(BaseViewSet): 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 @@ class IssueVotePublicViewSet(BaseViewSet): ) -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_export_task.delay( - email=request.user.email, data=request.data, slug=slug ,exporter_name=request.user.first_name + 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, + ) + + +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 2abc82631..75b94f034 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/api/views/notification.py @@ -10,7 +10,13 @@ from plane.utils.paginator import BasePaginator # 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 @@ class NotificationViewSet(BaseViewSet, BasePaginator): # 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 @@ class UnreadNotificationEndpoint(BaseAPIView): {"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 6adee0016..a83bbca25 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -11,14 +11,8 @@ from django.db.models import ( OuterRef, Func, F, - Max, - CharField, Func, Subquery, - Prefetch, - When, - Case, - Value, ) from django.core.validators import validate_email from django.conf import settings @@ -47,6 +41,7 @@ from plane.api.permissions import ( ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, + ProjectLitePermission, ) from plane.db.models import ( @@ -71,16 +66,9 @@ from plane.db.models import ( ModuleMember, Inbox, ProjectDeployBoard, - Issue, - IssueReaction, - IssueLink, - IssueAttachment, - Label, ) from plane.bgtasks.project_invitation_task import project_invitation -from plane.utils.grouper import group_results -from plane.utils.issue_filters import issue_filters class ProjectViewSet(BaseViewSet): @@ -287,7 +275,10 @@ class ProjectViewSet(BaseViewSet): ) 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 @@ class ProjectMemberViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) except ProjectMember.DoesNotExist: return Response( - {"error": "Project Member does not exist"}, status=status.HTTP_400 + {"error": "Project Member does not exist"}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: capture_exception(e) @@ -1140,145 +1131,78 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): ) -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")) + projects = ( + Project.objects.filter(workspace__slug=slug) .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") + 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", ) - # 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) - + return Response(projects, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) return Response( - { - "issues": issues, - "states": states, - "labels": labels, - }, - status=status.HTTP_200_OK, + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, ) - except ProjectDeployBoard.DoesNotExist: + + +class LeaveProjectEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] + + def delete(self, request, slug, project_id): + try: + project_member = ProjectMember.objects.get( + workspace__slug=slug, + member=request.user, + project_id=project_id, + ) + + # Only Admin case + if ( + project_member.role == 20 + and ProjectMember.objects.filter( + workspace__slug=slug, + role=20, + project_id=project_id, + ).count() + == 1 + ): + return Response( + { + "error": "You cannot leave the project since you are the only admin of the project you should delete the project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Delete the member from workspace + project_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except ProjectMember.DoesNotExist: return Response( - {"error": "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 84ee47e42..68958e504 100644 --- a/apiserver/plane/api/views/user.py +++ b/apiserver/plane/api/views/user.py @@ -137,11 +137,11 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView): 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 cfdd0dd9b..ce425185e 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1100,7 +1100,6 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView): 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 @@ class WorkspaceUserActivityEndpoint(BaseAPIView): 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 @@ class WorkspaceMembersEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class LeaveWorkspaceEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def delete(self, request, slug): + try: + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + + # Only Admin case + if ( + workspace_member.role == 20 + and WorkspaceMember.objects.filter( + workspace__slug=slug, role=20 + ).count() + == 1 + ): + return Response( + { + "error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Delete the member from workspace + workspace_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except WorkspaceMember.DoesNotExist: + return Response( + {"error": "Workspace member does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index 22a9afe51..a45120eb5 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -4,6 +4,7 @@ import io 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" - - 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, - ) + + 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"}, + ) + + 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 799904347..a77d68b4b 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 1cc6c85cc..0cadac553 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -24,6 +24,9 @@ from plane.db.models import ( 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 0e3ead65d..a1f4a3e92 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 000000000..01af46d20 --- /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 659eea3eb..90532dc64 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -19,6 +19,7 @@ from .project import ( ProjectIdentifier, ProjectFavorite, ProjectDeployBoard, + ProjectPublicMember, ) from .issue import ( diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 7af9e6e14..78e958380 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 0c2b5cb96..da155af40 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -254,3 +254,18 @@ class ProjectDeployBoard(ProjectBaseModel): 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 3975a3b93..e90e19c5e 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.db.models.signals import post_save 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 59e0bd31b..27da44d9c 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -49,7 +49,7 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "crum.CurrentRequestUserMiddleware", "django.middleware.gzip.GZipMiddleware", -] + ] REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( @@ -161,7 +161,7 @@ MEDIA_URL = "/media/" 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 fefae710d..d5831c54f 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 01c371911..000000000 --- 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 7e8fd0d26..000000000 --- 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 a4f426a23..000000000 --- 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 52fd1fe52..000000000 --- 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 fd43c733e..000000000 --- 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 320f4355f..000000000 --- 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 ee0024fa0..000000000 --- a/apps/app/components/icons/blocked-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const BlockedIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - - ); diff --git a/apps/app/components/icons/blocker-icon.tsx b/apps/app/components/icons/blocker-icon.tsx deleted file mode 100644 index 093728cd8..000000000 --- a/apps/app/components/icons/blocker-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const BlockerIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - - ); diff --git a/apps/app/components/icons/bolt-icon.tsx b/apps/app/components/icons/bolt-icon.tsx deleted file mode 100644 index 569767aa5..000000000 --- a/apps/app/components/icons/bolt-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const BoltIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/cancel-icon.tsx b/apps/app/components/icons/cancel-icon.tsx deleted file mode 100644 index c3170ca32..000000000 --- a/apps/app/components/icons/cancel-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CancelIcon: React.FC = ({ width, height, className }) => ( - - - - ); diff --git a/apps/app/components/icons/clipboard-icon.tsx b/apps/app/components/icons/clipboard-icon.tsx deleted file mode 100644 index c96aa3fde..000000000 --- a/apps/app/components/icons/clipboard-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const ClipboardIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/comment-icon.tsx b/apps/app/components/icons/comment-icon.tsx deleted file mode 100644 index c60cca4a6..000000000 --- a/apps/app/components/icons/comment-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CommentIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/completed-cycle-icon.tsx b/apps/app/components/icons/completed-cycle-icon.tsx deleted file mode 100644 index 615fbcb9a..000000000 --- a/apps/app/components/icons/completed-cycle-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CompletedCycleIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( - - - - ); diff --git a/apps/app/components/icons/current-cycle-icon.tsx b/apps/app/components/icons/current-cycle-icon.tsx deleted file mode 100644 index 2b07edf2e..000000000 --- a/apps/app/components/icons/current-cycle-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const CurrentCycleIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( - - - - ); diff --git a/apps/app/components/icons/edit-icon.tsx b/apps/app/components/icons/edit-icon.tsx deleted file mode 100644 index c4e012e4d..000000000 --- a/apps/app/components/icons/edit-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const EditIcon: React.FC = ({ width, height, className }) => ( - - - - ); diff --git a/apps/app/components/icons/ellipsis-horizontal-icon.tsx b/apps/app/components/icons/ellipsis-horizontal-icon.tsx deleted file mode 100644 index cfdd66751..000000000 --- a/apps/app/components/icons/ellipsis-horizontal-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const EllipsisHorizontalIcon: React.FC = ({ width, height, className }) => ( - - - - ); diff --git a/apps/app/components/icons/lock-icon.tsx b/apps/app/components/icons/lock-icon.tsx deleted file mode 100644 index d0c9cffb7..000000000 --- a/apps/app/components/icons/lock-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const LockIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/menu-icon.tsx b/apps/app/components/icons/menu-icon.tsx deleted file mode 100644 index 0a8816b75..000000000 --- a/apps/app/components/icons/menu-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const MenuIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/plus-icon.tsx b/apps/app/components/icons/plus-icon.tsx deleted file mode 100644 index 0b958a21d..000000000 --- a/apps/app/components/icons/plus-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const PlusIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/question-mark-circle-icon.tsx b/apps/app/components/icons/question-mark-circle-icon.tsx deleted file mode 100644 index 2cdf9d8e5..000000000 --- a/apps/app/components/icons/question-mark-circle-icon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const QuestionMarkCircleIcon: React.FC = ({ - width = "24", - height = "24", - className, -}) => ( - - - - ); diff --git a/apps/app/components/icons/signal-cellular-icon.tsx b/apps/app/components/icons/signal-cellular-icon.tsx deleted file mode 100644 index 0e785d958..000000000 --- a/apps/app/components/icons/signal-cellular-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const SignalCellularIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/tag-icon.tsx b/apps/app/components/icons/tag-icon.tsx deleted file mode 100644 index a17d4c1e4..000000000 --- a/apps/app/components/icons/tag-icon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const TagIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( - - - - ); diff --git a/apps/app/components/icons/transfer-icon.tsx b/apps/app/components/icons/transfer-icon.tsx deleted file mode 100644 index 176c38b29..000000000 --- a/apps/app/components/icons/transfer-icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const TransferIcon: React.FC = ({ width, height, className, color }) => ( - - - - ); diff --git a/apps/app/components/icons/tune-icon.tsx b/apps/app/components/icons/tune-icon.tsx deleted file mode 100644 index 1221b2976..000000000 --- a/apps/app/components/icons/tune-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const TuneIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/upcoming-cycle-icon.tsx b/apps/app/components/icons/upcoming-cycle-icon.tsx deleted file mode 100644 index 52961e15e..000000000 --- a/apps/app/components/icons/upcoming-cycle-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const UpcomingCycleIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( - - - - ); diff --git a/apps/app/components/icons/user-icon-circle.tsx b/apps/app/components/icons/user-icon-circle.tsx deleted file mode 100644 index 8bae34133..000000000 --- a/apps/app/components/icons/user-icon-circle.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const UserCircleIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/icons/user-icon.tsx b/apps/app/components/icons/user-icon.tsx deleted file mode 100644 index c0408dad3..000000000 --- a/apps/app/components/icons/user-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -import type { Props } from "./types"; - -export const UserIcon: React.FC = ({ width = "24", height = "24", className }) => ( - - - - ); diff --git a/apps/app/components/integration/slack/index.ts b/apps/app/components/integration/slack/index.ts deleted file mode 100644 index 3bd1c965c..000000000 --- a/apps/app/components/integration/slack/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./select-channel"; \ No newline at end of file diff --git a/apps/app/components/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx deleted file mode 100644 index 9d00bf784..000000000 --- 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 323d201e8..000000000 --- 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 1eacba245..000000000 --- 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 ab8bfac15..000000000 --- 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 a8770e03d..000000000 --- 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 1e780dd57..000000000 --- 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 a97fe06fb..000000000 --- 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 b1751b070..000000000 --- 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 5f9d9ae2c..000000000 --- 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 7bfca0d2c..000000000 --- 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 1b327227a..000000000 --- a/apps/app/components/workspace/issues-stats.tsx +++ /dev/null @@ -1,83 +0,0 @@ -// components -import { ActivityGraph } from "components/workspace"; -// ui -import { Loader, Tooltip } from "components/ui"; -// icons -import { InformationCircleIcon } from "@heroicons/react/24/outline"; -// types -import { IUserWorkspaceDashboard } from "types"; - -type Props = { - data: IUserWorkspaceDashboard | undefined; -}; - -export const IssuesStats: React.FC = ({ data }) => ( -
-
-
-
-

Issues assigned to you

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

Pending issues

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

Completed issues

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

Issues due by this week

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

- Activity Graph - - - -

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

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

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

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

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