diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 000000000..ed3814532 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,84 @@ +name: Auto Merge or Create PR on Push + +on: + workflow_dispatch: + push: + branches: + - "sync/**" + +env: + CURRENT_BRANCH: ${{ github.ref_name }} + SOURCE_BRANCH: ${{ secrets.SYNC_SOURCE_BRANCH_NAME }} # The sync branch such as "sync/ce" + TARGET_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The target branch that you would like to merge changes like develop + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows + REVIEWER: ${{ secrets.SYNC_PR_REVIEWER }} + +jobs: + Check_Branch: + runs-on: ubuntu-latest + outputs: + BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }} + steps: + - name: Check if current branch matches the secret + id: check-branch + run: | + if [ "$CURRENT_BRANCH" = "$SOURCE_BRANCH" ]; then + echo "MATCH=true" >> $GITHUB_OUTPUT + else + echo "MATCH=false" >> $GITHUB_OUTPUT + fi + + Auto_Merge: + if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }} + needs: [Check_Branch] + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 # Fetch all history for all branches and tags + + - name: Setup Git + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + + - name: Setup GH CLI and Git Config + run: | + type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + sudo apt update + sudo apt install gh -y + + - name: Check for merge conflicts + id: conflicts + run: | + git fetch origin $TARGET_BRANCH + git checkout $TARGET_BRANCH + # Attempt to merge the main branch into the current branch + if $(git merge --no-commit --no-ff $SOURCE_BRANCH); then + echo "No merge conflicts detected." + echo "HAS_CONFLICTS=false" >> $GITHUB_ENV + else + echo "Merge conflicts detected." + echo "HAS_CONFLICTS=true" >> $GITHUB_ENV + git merge --abort + fi + + - name: Merge Change to Target Branch + if: env.HAS_CONFLICTS == 'false' + run: | + git commit -m "Merge branch '$SOURCE_BRANCH' into $TARGET_BRANCH" + git push origin $TARGET_BRANCH + + - name: Create PR to Target Branch + if: env.HAS_CONFLICTS == 'true' + run: | + # Replace 'username' with the actual GitHub username of the reviewer. + PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: merge conflicts need to be resolved" --body "" --reviewer $REVIEWER) + echo "Pull Request created: $PR_URL" diff --git a/.github/workflows/build-test-pull-request.yml b/.github/workflows/build-test-pull-request.yml index 83ed41625..e0014f696 100644 --- a/.github/workflows/build-test-pull-request.yml +++ b/.github/workflows/build-test-pull-request.yml @@ -1,27 +1,19 @@ -name: Build Pull Request Contents +name: Build and Lint on Pull Request on: + workflow_dispatch: pull_request: types: ["opened", "synchronize"] jobs: - build-pull-request-contents: - name: Build Pull Request Contents - runs-on: ubuntu-20.04 - permissions: - pull-requests: read - + get-changed-files: + runs-on: ubuntu-latest + outputs: + apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }} + web_changed: ${{ steps.changed-files.outputs.web_any_changed }} + space_changed: ${{ steps.changed-files.outputs.deploy_any_changed }} steps: - - name: Checkout Repository to Actions - uses: actions/checkout@v3.3.0 - with: - token: ${{ secrets.ACCESS_TOKEN }} - - - name: Setup Node.js 18.x - uses: actions/setup-node@v2 - with: - node-version: 18.x - + - uses: actions/checkout@v3 - name: Get changed files id: changed-files uses: tj-actions/changed-files@v41 @@ -31,17 +23,82 @@ jobs: - apiserver/** web: - web/** + - packages/** + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - 'turbo.json' deploy: - space/** + - packages/** + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - 'turbo.json' - - name: Build Plane's Main App - if: steps.changed-files.outputs.web_any_changed == 'true' - run: | - yarn - yarn build --filter=web + lint-apiserver: + needs: get-changed-files + runs-on: ubuntu-latest + if: needs.get-changed-files.outputs.apiserver_changed == 'true' + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' # Specify the Python version you need + - name: Install Pylint + run: python -m pip install ruff + - name: Install Apiserver Dependencies + run: cd apiserver && pip install -r requirements.txt + - name: Lint apiserver + run: ruff check --fix apiserver - - name: Build Plane's Deploy App - if: steps.changed-files.outputs.deploy_any_changed == 'true' - run: | - yarn - yarn build --filter=space + lint-web: + needs: get-changed-files + if: needs.get-changed-files.outputs.web_changed == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn lint --filter=web + + lint-space: + needs: get-changed-files + if: needs.get-changed-files.outputs.space_changed == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn lint --filter=space + + build-web: + needs: lint-web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn build --filter=web + + build-space: + needs: lint-space + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + - run: yarn install + - run: yarn build --filter=space diff --git a/.github/workflows/check-version.yml b/.github/workflows/check-version.yml new file mode 100644 index 000000000..ca8b6f8b3 --- /dev/null +++ b/.github/workflows/check-version.yml @@ -0,0 +1,45 @@ +name: Version Change Before Release + +on: + pull_request: + branches: + - master + +jobs: + check-version: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Get PR Branch version + run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV + + - name: Fetch base branch + run: git fetch origin master:master + + - name: Get Master Branch version + run: | + git checkout master + echo "MASTER_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV + + - name: Get master branch version and compare + run: | + echo "Comparing versions: PR version is $PR_VERSION, Master version is $MASTER_VERSION" + if [ "$PR_VERSION" == "$MASTER_VERSION" ]; then + echo "Version in PR branch is the same as in master. Failing the CI." + exit 1 + else + echo "Version check passed. Versions are different." + fi + env: + PR_VERSION: ${{ env.PR_VERSION }} + MASTER_VERSION: ${{ env.MASTER_VERSION }} diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml new file mode 100644 index 000000000..7b9f5ffcc --- /dev/null +++ b/.github/workflows/feature-deployment.yml @@ -0,0 +1,73 @@ +name: Feature Preview + +on: + workflow_dispatch: + inputs: + web-build: + required: true + type: boolean + default: true + space-build: + required: true + type: boolean + default: false + +jobs: + feature-deploy: + name: Feature Deploy + runs-on: ubuntu-latest + env: + KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG }} + BUILD_WEB: ${{ (github.event.inputs.web-build == '' && true) || github.event.inputs.web-build }} + BUILD_SPACE: ${{ (github.event.inputs.space-build == '' && false) || github.event.inputs.space-build }} + + steps: + - name: Tailscale + uses: tailscale/github-action@v2 + with: + oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }} + oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} + tags: tag:ci + + - name: Kubectl Setup + run: | + curl -LO "https://dl.k8s.io/release/${{secrets.KUBE_VERSION}}/bin/linux/amd64/kubectl" + chmod +x kubectl + + mkdir -p ~/.kube + echo "$KUBE_CONFIG_FILE" > ~/.kube/config + chmod 600 ~/.kube/config + + - name: HELM Setup + run: | + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + + - name: App Deploy + run: | + helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ secrets.FEATURE_PREVIEW_HELM_CHART_URL }} + GIT_BRANCH=${{ github.ref_name }} + APP_NAMESPACE=${{ secrets.FEATURE_PREVIEW_NAMESPACE }} + + METADATA=$(helm install feature-preview/${{ secrets.FEATURE_PREVIEW_HELM_CHART_NAME }} \ + --kube-insecure-skip-tls-verify \ + --generate-name \ + --namespace $APP_NAMESPACE \ + --set shared_config.git_repo=${{github.server_url}}/${{ github.repository }}.git \ + --set shared_config.git_branch="$GIT_BRANCH" \ + --set web.enabled=${{ env.BUILD_WEB }} \ + --set space.enabled=${{ env.BUILD_SPACE }} \ + --output json \ + --timeout 1000s) + + APP_NAME=$(echo $METADATA | jq -r '.name') + + INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \ + -o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \ + jq -r '.spec.rules[0].host') + + echo "****************************************" + echo "APP NAME ::: $APP_NAME" + echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME" + echo "****************************************" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 148568d76..f40c1a244 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,6 @@ chmod +x setup.sh docker compose -f docker-compose-local.yml up ``` - ## Missing a Feature? If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository. diff --git a/ENV_SETUP.md b/ENV_SETUP.md index bfc300196..df05683ef 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -53,7 +53,6 @@ NGINX_PORT=80 NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces" ``` - ## {PROJECT_FOLDER}/apiserver/.env ​ diff --git a/README.md b/README.md index b509fd6f6..6834199ff 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

Plane

-

Flexible, extensible open-source project management

+

Open-source project management that unlocks customer value.

@@ -16,6 +16,13 @@ Commit activity per month

+

+ Website • + Releases • + Twitter • + Documentation +

+

-Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘‍♀️. +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/docker-compose). +## ⚡ Installation -## ⚡️ Contributors Quick Start +The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users. -### Prerequisite +If you want more control over your data prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose). -Development system must have docker engine installed and running. +| Installation Methods | Documentation Link | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) | +| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) | -### Steps - -Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute - -1. Clone the code locally using `git clone https://github.com/makeplane/plane.git` -1. Switch to the code folder `cd plane` -1. Create your feature or fix branch you plan to work on using `git checkout -b ` -1. Open terminal and run `./setup.sh` -1. Open the code on VSCode or similar equivalent IDE -1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system -1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d` - -You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload) - -Thats it! - -## 🍙 Self Hosting - -For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/docker-compose) documentation page +`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature. ## 🚀 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. +- **Issues**: Quickly create issues and add details using a powerful, rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking. + +- **Cycles** + Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features. + +- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily. + - **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. + +- **Pages**: Plane pages, equipped with AI and a rich text editor, let you jot down your thoughts on the fly. Format your text, upload images, hyperlink, or sync your existing ideas into an actionable item or issue. + +- **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work. + +- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution. + +## 🛠️ Contributors Quick Start + +> Development system must have docker engine installed and running. + +Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute + +1. Clone the code locally using: + ``` + git clone https://github.com/makeplane/plane.git + ``` +2. Switch to the code folder: + ``` + cd plane + ``` +3. Create your feature or fix branch you plan to work on using: + ``` + git checkout -b + ``` +4. Open terminal and run: + ``` + ./setup.sh + ``` +5. Open the code on VSCode or similar equivalent IDE. +6. Review the `.env` files available in various folders. + Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system. +7. Run the docker command to initiate services: + ``` + docker compose -f docker-compose-local.yml up -d + ``` + +You are ready to make changes to the code. Do not forget to refresh the browser (in case it does not auto-reload). + +Thats it! + +## ❤️ Community + +The Plane community can be found on [GitHub Discussions](https://github.com/orgs/makeplane/discussions), and our [Discord server](https://discord.com/invite/A92xrEGCge). Our [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community chanels. + +Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects. + +### Repo Activity + +![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image") ## 📸 Screenshots

Plane Views @@ -91,8 +132,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Issue Details @@ -100,7 +140,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Cycles and Modules @@ -109,7 +149,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Analytics @@ -118,7 +158,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Pages @@ -128,7 +168,7 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

Plane Command Menu @@ -136,20 +176,23 @@ For self hosting environment setup, visit the [Self Hosting](https://docs.plane.

-## 📚Documentation - -For full documentation, visit [docs.plane.so](https://docs.plane.so/) - -To see how to Contribute, visit [here](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md). - -## ❤️ Community - -The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects. - -To chat with other community members you can join the [Plane Discord](https://discord.com/invite/A92xrEGCge). - -Our [Code of Conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community channels. - ## ⛓️ Security -If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. Email engineering@plane.so to disclose any security vulnerabilities. +If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports. + +Email squawk@plane.so to disclose any security vulnerabilities. + +## ❤️ Contribute + +There are many ways to contribute to Plane, including: + +- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components. +- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features. +- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)! +- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support. + +### We couldn't have done this without you. + +
+ + diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 4c8d6e815..b8f194b32 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -1,8 +1,9 @@ from lxml import html - # Django imports from django.utils import timezone +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError # Third party imports from rest_framework import serializers @@ -284,6 +285,20 @@ class IssueLinkSerializer(BaseSerializer): "updated_at", ] + def validate_url(self, value): + # Check URL format + validate_url = URLValidator() + try: + validate_url(value) + except ValidationError: + raise serializers.ValidationError("Invalid URL format.") + + # Check URL scheme + if not value.startswith(('http://', 'https://')): + raise serializers.ValidationError("Invalid URL scheme.") + + return value + # Validation if url already exists def create(self, validated_data): if IssueLink.objects.filter( @@ -295,6 +310,17 @@ class IssueLinkSerializer(BaseSerializer): ) return IssueLink.objects.create(**validated_data) + def update(self, instance, validated_data): + if IssueLink.objects.filter( + url=validated_data.get("url"), + issue_id=instance.issue_id, + ).exists(): + raise serializers.ValidationError( + {"error": "URL already exists for this Issue"} + ) + + return super().update(instance, validated_data) + class IssueAttachmentSerializer(BaseSerializer): class Meta: diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 342cc1a81..9dd4c9b85 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -6,8 +6,6 @@ from plane.db.models import ( Project, ProjectIdentifier, WorkspaceMember, - State, - Estimate, ) from .base import BaseSerializer diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index edb89f9b1..146f61f48 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -1,6 +1,5 @@ # Python imports import zoneinfo -import json from urllib.parse import urlparse @@ -115,13 +114,13 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): if isinstance(e, ObjectDoesNotExist): return Response( - {"error": f"The required object does not exist."}, + {"error": "The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) if isinstance(e, KeyError): return Response( - {"error": f" The required key does not exist."}, + {"error": " The required key does not exist."}, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 84931f46b..2ae7faea4 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -2,7 +2,7 @@ import json # Django imports -from django.db.models import Q, Count, Sum, Prefetch, F, OuterRef, Func +from django.db.models import Q, Count, Sum, F, OuterRef, Func from django.utils import timezone from django.core import serializers @@ -321,7 +321,9 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView): and Cycle.objects.filter( project_id=project_id, workspace__slug=slug, - external_source=request.data.get("external_source", cycle.external_source), + external_source=request.data.get( + "external_source", cycle.external_source + ), external_id=request.data.get("external_id"), ).exists() ): diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index c1079345a..fb36ea2a9 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -119,7 +119,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): ) # Check for valid priority - if not request.data.get("issue", {}).get("priority", "none") in [ + if request.data.get("issue", {}).get("priority", "none") not in [ "low", "medium", "high", diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 0905ae1f7..e2ef742b9 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -1,22 +1,22 @@ # Python imports import json -from itertools import chain + +from django.core.serializers.json import DjangoJSONEncoder # Django imports from django.db import IntegrityError from django.db.models import ( - OuterRef, - Func, - Q, - F, Case, - When, - Value, CharField, - Max, Exists, + F, + Func, + Max, + OuterRef, + Q, + Value, + When, ) -from django.core.serializers.json import DjangoJSONEncoder from django.utils import timezone # Third party imports @@ -24,30 +24,31 @@ from rest_framework import status from rest_framework.response import Response # Module imports -from .base import BaseAPIView, WebhookMixin -from plane.app.permissions import ( - ProjectEntityPermission, - ProjectMemberPermission, - ProjectLitePermission, -) -from plane.db.models import ( - Issue, - IssueAttachment, - IssueLink, - Project, - Label, - ProjectMember, - IssueComment, - IssueActivity, -) -from plane.bgtasks.issue_activites_task import issue_activity from plane.api.serializers import ( + IssueActivitySerializer, + IssueCommentSerializer, + IssueLinkSerializer, IssueSerializer, LabelSerializer, - IssueLinkSerializer, - IssueCommentSerializer, - IssueActivitySerializer, ) +from plane.app.permissions import ( + ProjectEntityPermission, + ProjectLitePermission, + ProjectMemberPermission, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.db.models import ( + Issue, + IssueActivity, + IssueAttachment, + IssueComment, + IssueLink, + Label, + Project, + ProjectMember, +) + +from .base import BaseAPIView, WebhookMixin class IssueAPIEndpoint(WebhookMixin, BaseAPIView): @@ -653,7 +654,6 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): ) def post(self, request, slug, project_id, issue_id): - # Validation check if the issue already exists if ( request.data.get("external_id") @@ -679,7 +679,6 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): status=status.HTTP_409_CONFLICT, ) - serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): serializer.save( @@ -716,9 +715,12 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): # Validation check if the issue already exists if ( - str(request.data.get("external_id")) - and (issue_comment.external_id != str(request.data.get("external_id"))) - and Issue.objects.filter( + request.data.get("external_id") + and ( + issue_comment.external_id + != str(request.data.get("external_id")) + ) + and IssueComment.objects.filter( project_id=project_id, workspace__slug=slug, external_source=request.data.get( @@ -735,7 +737,6 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): status=status.HTTP_409_CONFLICT, ) - serializer = IssueCommentSerializer( issue_comment, data=request.data, partial=True ) diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 2e5bb85e2..677f65ff8 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -178,7 +178,9 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): and Module.objects.filter( project_id=project_id, workspace__slug=slug, - external_source=request.data.get("external_source", module.external_source), + external_source=request.data.get( + "external_source", module.external_source + ), external_id=request.data.get("external_id"), ).exists() ): diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index cb1f7dc7b..e994dfbec 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -11,7 +11,6 @@ from rest_framework.serializers import ValidationError from plane.db.models import ( Workspace, Project, - ProjectFavorite, ProjectMember, ProjectDeployBoard, State, @@ -150,7 +149,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): serializer.save() # Add the user as Administrator to the project - project_member = ProjectMember.objects.create( + _ = ProjectMember.objects.create( project_id=serializer.data["id"], member=request.user, role=20, @@ -245,12 +244,12 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): {"name": "The project name is already taken"}, status=status.HTTP_410_GONE, ) - except Workspace.DoesNotExist as e: + except Workspace.DoesNotExist: return Response( {"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND, ) - except ValidationError as e: + except ValidationError: return Response( {"identifier": "The project identifier is already taken"}, status=status.HTTP_410_GONE, @@ -307,7 +306,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND, ) - except ValidationError as e: + except ValidationError: return Response( {"identifier": "The project identifier is already taken"}, status=status.HTTP_410_GONE, diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index ec10f9bab..53ed5d6b7 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -66,8 +66,10 @@ class StateAPIEndpoint(BaseAPIView): serializer.save(project_id=project_id) return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except IntegrityError as e: + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + except IntegrityError: state = State.objects.filter( workspace__slug=slug, project_id=project_id, @@ -136,7 +138,9 @@ class StateAPIEndpoint(BaseAPIView): and State.objects.filter( project_id=project_id, workspace__slug=slug, - external_source=request.data.get("external_source", state.external_source), + external_source=request.data.get( + "external_source", state.external_source + ), external_id=request.data.get("external_id"), ).exists() ): diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 9bdd4baaf..22673dabc 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -86,16 +86,6 @@ from .module import ( from .api import APITokenSerializer, APITokenReadSerializer -from .integration import ( - IntegrationSerializer, - WorkspaceIntegrationSerializer, - GithubIssueSyncSerializer, - GithubRepositorySerializer, - GithubRepositorySyncSerializer, - GithubCommentSyncSerializer, - SlackProjectSyncSerializer, -) - from .importer import ImporterSerializer from .page import ( @@ -121,7 +111,10 @@ from .inbox import ( from .analytic import AnalyticViewSerializer -from .notification import NotificationSerializer, UserNotificationPreferenceSerializer +from .notification import ( + NotificationSerializer, + UserNotificationPreferenceSerializer, +) from .exporter import ExporterHistorySerializer diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index a273b349c..30e6237f1 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -11,6 +11,7 @@ from plane.db.models import ( CycleUserProperties, ) + class CycleWriteSerializer(BaseSerializer): def validate(self, data): if ( @@ -47,7 +48,6 @@ class CycleSerializer(BaseSerializer): # active | draft | upcoming | completed status = serializers.CharField(read_only=True) - class Meta: model = Cycle fields = [ diff --git a/apiserver/plane/app/serializers/dashboard.py b/apiserver/plane/app/serializers/dashboard.py index 8fca3c906..b0ed8841b 100644 --- a/apiserver/plane/app/serializers/dashboard.py +++ b/apiserver/plane/app/serializers/dashboard.py @@ -18,9 +18,4 @@ class WidgetSerializer(BaseSerializer): class Meta: model = Widget - fields = [ - "id", - "key", - "is_visible", - "widget_filters" - ] \ No newline at end of file + fields = ["id", "key", "is_visible", "widget_filters"] diff --git a/apiserver/plane/app/serializers/estimate.py b/apiserver/plane/app/serializers/estimate.py index 675390080..d28f38c75 100644 --- a/apiserver/plane/app/serializers/estimate.py +++ b/apiserver/plane/app/serializers/estimate.py @@ -74,5 +74,3 @@ class WorkspaceEstimateSerializer(BaseSerializer): "name", "description", ] - - diff --git a/apiserver/plane/app/serializers/integration/__init__.py b/apiserver/plane/app/serializers/integration/__init__.py deleted file mode 100644 index 112ff02d1..000000000 --- a/apiserver/plane/app/serializers/integration/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .base import IntegrationSerializer, WorkspaceIntegrationSerializer -from .github import ( - GithubRepositorySerializer, - GithubRepositorySyncSerializer, - GithubIssueSyncSerializer, - GithubCommentSyncSerializer, -) -from .slack import SlackProjectSyncSerializer diff --git a/apiserver/plane/app/serializers/integration/base.py b/apiserver/plane/app/serializers/integration/base.py deleted file mode 100644 index 01e484ed0..000000000 --- a/apiserver/plane/app/serializers/integration/base.py +++ /dev/null @@ -1,22 +0,0 @@ -# Module imports -from plane.app.serializers import BaseSerializer -from plane.db.models import Integration, WorkspaceIntegration - - -class IntegrationSerializer(BaseSerializer): - class Meta: - model = Integration - fields = "__all__" - read_only_fields = [ - "verified", - ] - - -class WorkspaceIntegrationSerializer(BaseSerializer): - integration_detail = IntegrationSerializer( - read_only=True, source="integration" - ) - - class Meta: - model = WorkspaceIntegration - fields = "__all__" diff --git a/apiserver/plane/app/serializers/integration/github.py b/apiserver/plane/app/serializers/integration/github.py deleted file mode 100644 index 850bccf1b..000000000 --- a/apiserver/plane/app/serializers/integration/github.py +++ /dev/null @@ -1,45 +0,0 @@ -# Module imports -from plane.app.serializers import BaseSerializer -from plane.db.models import ( - GithubIssueSync, - GithubRepository, - GithubRepositorySync, - GithubCommentSync, -) - - -class GithubRepositorySerializer(BaseSerializer): - class Meta: - model = GithubRepository - fields = "__all__" - - -class GithubRepositorySyncSerializer(BaseSerializer): - repo_detail = GithubRepositorySerializer(source="repository") - - class Meta: - model = GithubRepositorySync - fields = "__all__" - - -class GithubIssueSyncSerializer(BaseSerializer): - class Meta: - model = GithubIssueSync - fields = "__all__" - read_only_fields = [ - "project", - "workspace", - "repository_sync", - ] - - -class GithubCommentSyncSerializer(BaseSerializer): - class Meta: - model = GithubCommentSync - fields = "__all__" - read_only_fields = [ - "project", - "workspace", - "repository_sync", - "issue_sync", - ] diff --git a/apiserver/plane/app/serializers/integration/slack.py b/apiserver/plane/app/serializers/integration/slack.py deleted file mode 100644 index 9c461c5b9..000000000 --- a/apiserver/plane/app/serializers/integration/slack.py +++ /dev/null @@ -1,14 +0,0 @@ -# Module imports -from plane.app.serializers import BaseSerializer -from plane.db.models import SlackProjectSync - - -class SlackProjectSyncSerializer(BaseSerializer): - class Meta: - model = SlackProjectSync - fields = "__all__" - read_only_fields = [ - "project", - "workspace", - "workspace_integration", - ] diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 411c5b73f..45f844cf0 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -1,5 +1,7 @@ # Django imports from django.utils import timezone +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError # Third Party imports from rest_framework import serializers @@ -7,7 +9,7 @@ from rest_framework import serializers # Module imports from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer -from .state import StateSerializer, StateLiteSerializer +from .state import StateLiteSerializer from .project import ProjectLiteSerializer from .workspace import WorkspaceLiteSerializer from plane.db.models import ( @@ -31,7 +33,6 @@ from plane.db.models import ( IssueVote, IssueRelation, State, - Project, ) @@ -432,6 +433,20 @@ class IssueLinkSerializer(BaseSerializer): "issue", ] + def validate_url(self, value): + # Check URL format + validate_url = URLValidator() + try: + validate_url(value) + except ValidationError: + raise serializers.ValidationError("Invalid URL format.") + + # Check URL scheme + if not value.startswith(('http://', 'https://')): + raise serializers.ValidationError("Invalid URL scheme.") + + return value + # Validation if url already exists def create(self, validated_data): if IssueLink.objects.filter( @@ -443,9 +458,19 @@ class IssueLinkSerializer(BaseSerializer): ) return IssueLink.objects.create(**validated_data) + def update(self, instance, validated_data): + if IssueLink.objects.filter( + url=validated_data.get("url"), + issue_id=instance.issue_id, + ).exists(): + raise serializers.ValidationError( + {"error": "URL already exists for this Issue"} + ) + + return super().update(instance, validated_data) + class IssueLinkLiteSerializer(BaseSerializer): - class Meta: model = IssueLink fields = [ @@ -476,7 +501,6 @@ class IssueAttachmentSerializer(BaseSerializer): class IssueAttachmentLiteSerializer(DynamicBaseSerializer): - class Meta: model = IssueAttachment fields = [ @@ -505,7 +529,6 @@ class IssueReactionSerializer(BaseSerializer): class IssueReactionLiteSerializer(DynamicBaseSerializer): - class Meta: model = IssueReaction fields = [ @@ -601,15 +624,18 @@ class IssueSerializer(DynamicBaseSerializer): # ids cycle_id = serializers.PrimaryKeyRelatedField(read_only=True) module_ids = serializers.ListField( - child=serializers.UUIDField(), required=False, + child=serializers.UUIDField(), + required=False, ) # Many to many label_ids = serializers.ListField( - child=serializers.UUIDField(), required=False, + child=serializers.UUIDField(), + required=False, ) assignee_ids = serializers.ListField( - child=serializers.UUIDField(), required=False, + child=serializers.UUIDField(), + required=False, ) # Count items @@ -649,19 +675,7 @@ class IssueSerializer(DynamicBaseSerializer): read_only_fields = fields -class IssueDetailSerializer(IssueSerializer): - description_html = serializers.CharField() - is_subscribed = serializers.BooleanField(read_only=True) - - class Meta(IssueSerializer.Meta): - fields = IssueSerializer.Meta.fields + [ - "description_html", - "is_subscribed", - ] - - class IssueLiteSerializer(DynamicBaseSerializer): - class Meta: model = Issue fields = [ diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index 4aabfc50e..3b2468aee 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -3,7 +3,6 @@ from rest_framework import serializers # Module imports from .base import BaseSerializer, DynamicBaseSerializer -from .user import UserLiteSerializer from .project import ProjectLiteSerializer from plane.db.models import ( @@ -142,7 +141,6 @@ class ModuleIssueSerializer(BaseSerializer): class ModuleLinkSerializer(BaseSerializer): - class Meta: model = ModuleLink fields = "__all__" @@ -215,13 +213,11 @@ class ModuleSerializer(DynamicBaseSerializer): read_only_fields = fields - class ModuleDetailSerializer(ModuleSerializer): - link_module = ModuleLinkSerializer(read_only=True, many=True) class Meta(ModuleSerializer.Meta): - fields = ModuleSerializer.Meta.fields + ['link_module'] + fields = ModuleSerializer.Meta.fields + ["link_module"] class ModuleFavoriteSerializer(BaseSerializer): diff --git a/apiserver/plane/app/serializers/notification.py b/apiserver/plane/app/serializers/notification.py index 2152fcf0f..c6713a354 100644 --- a/apiserver/plane/app/serializers/notification.py +++ b/apiserver/plane/app/serializers/notification.py @@ -15,7 +15,6 @@ class NotificationSerializer(BaseSerializer): class UserNotificationPreferenceSerializer(BaseSerializer): - class Meta: model = UserNotificationPreference fields = "__all__" diff --git a/apiserver/plane/app/serializers/page.py b/apiserver/plane/app/serializers/page.py index a0f5986d6..4dfe6ea9d 100644 --- a/apiserver/plane/app/serializers/page.py +++ b/apiserver/plane/app/serializers/page.py @@ -3,7 +3,7 @@ from rest_framework import serializers # Module imports from .base import BaseSerializer -from .issue import IssueFlatSerializer, LabelLiteSerializer +from .issue import LabelLiteSerializer from .workspace import WorkspaceLiteSerializer from .project import ProjectLiteSerializer from plane.db.models import ( @@ -12,8 +12,6 @@ from plane.db.models import ( PageFavorite, PageLabel, Label, - Issue, - Module, ) diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index 999233442..6840fa8f7 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -95,8 +95,7 @@ class ProjectLiteSerializer(BaseSerializer): "identifier", "name", "cover_image", - "icon_prop", - "emoji", + "logo_props", "description", ] read_only_fields = fields diff --git a/apiserver/plane/app/serializers/user.py b/apiserver/plane/app/serializers/user.py index 8cd48827e..d6c15ee7f 100644 --- a/apiserver/plane/app/serializers/user.py +++ b/apiserver/plane/app/serializers/user.py @@ -4,7 +4,6 @@ from rest_framework import serializers # Module import from .base import BaseSerializer from plane.db.models import User, Workspace, WorkspaceMemberInvite -from plane.license.models import InstanceAdmin, Instance class UserSerializer(BaseSerializer): @@ -99,13 +98,13 @@ class UserMeSettingsSerializer(BaseSerializer): ).first() return { "last_workspace_id": obj.last_workspace_id, - "last_workspace_slug": workspace.slug - if workspace is not None - else "", + "last_workspace_slug": ( + workspace.slug if workspace is not None else "" + ), "fallback_workspace_id": obj.last_workspace_id, - "fallback_workspace_slug": workspace.slug - if workspace is not None - else "", + "fallback_workspace_slug": ( + workspace.slug if workspace is not None else "" + ), "invites": workspace_invites, } else: @@ -120,12 +119,16 @@ class UserMeSettingsSerializer(BaseSerializer): return { "last_workspace_id": None, "last_workspace_slug": None, - "fallback_workspace_id": fallback_workspace.id - if fallback_workspace is not None - else None, - "fallback_workspace_slug": fallback_workspace.slug - if fallback_workspace is not None - else None, + "fallback_workspace_id": ( + fallback_workspace.id + if fallback_workspace is not None + else None + ), + "fallback_workspace_slug": ( + fallback_workspace.slug + if fallback_workspace is not None + else None + ), "invites": workspace_invites, } diff --git a/apiserver/plane/app/serializers/webhook.py b/apiserver/plane/app/serializers/webhook.py index 95ca149ff..175dea304 100644 --- a/apiserver/plane/app/serializers/webhook.py +++ b/apiserver/plane/app/serializers/webhook.py @@ -1,5 +1,4 @@ # Python imports -import urllib import socket import ipaddress from urllib.parse import urlparse diff --git a/apiserver/plane/app/urls/__init__.py b/apiserver/plane/app/urls/__init__.py index f2b11f127..40b96687d 100644 --- a/apiserver/plane/app/urls/__init__.py +++ b/apiserver/plane/app/urls/__init__.py @@ -6,9 +6,7 @@ from .cycle import urlpatterns as cycle_urls from .dashboard import urlpatterns as dashboard_urls from .estimate import urlpatterns as estimate_urls from .external import urlpatterns as external_urls -from .importer import urlpatterns as importer_urls from .inbox import urlpatterns as inbox_urls -from .integration import urlpatterns as integration_urls from .issue import urlpatterns as issue_urls from .module import urlpatterns as module_urls from .notification import urlpatterns as notification_urls @@ -32,9 +30,7 @@ urlpatterns = [ *dashboard_urls, *estimate_urls, *external_urls, - *importer_urls, *inbox_urls, - *integration_urls, *issue_urls, *module_urls, *notification_urls, diff --git a/apiserver/plane/app/urls/external.py b/apiserver/plane/app/urls/external.py index 774e6fb7c..8db87a249 100644 --- a/apiserver/plane/app/urls/external.py +++ b/apiserver/plane/app/urls/external.py @@ -2,7 +2,6 @@ from django.urls import path from plane.app.views import UnsplashEndpoint -from plane.app.views import ReleaseNotesEndpoint from plane.app.views import GPTIntegrationEndpoint @@ -12,11 +11,6 @@ urlpatterns = [ UnsplashEndpoint.as_view(), name="unsplash", ), - path( - "release-notes/", - ReleaseNotesEndpoint.as_view(), - name="release-notes", - ), path( "workspaces//projects//ai-assistant/", GPTIntegrationEndpoint.as_view(), diff --git a/apiserver/plane/app/urls/importer.py b/apiserver/plane/app/urls/importer.py deleted file mode 100644 index f3a018d78..000000000 --- a/apiserver/plane/app/urls/importer.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.urls import path - - -from plane.app.views import ( - ServiceIssueImportSummaryEndpoint, - ImportServiceEndpoint, - UpdateServiceImportStatusEndpoint, -) - - -urlpatterns = [ - path( - "workspaces//importers//", - ServiceIssueImportSummaryEndpoint.as_view(), - name="importer-summary", - ), - path( - "workspaces//projects/importers//", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers/", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers///", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//projects//service//importers//", - UpdateServiceImportStatusEndpoint.as_view(), - name="importer-status", - ), -] diff --git a/apiserver/plane/app/urls/integration.py b/apiserver/plane/app/urls/integration.py deleted file mode 100644 index cf3f82d5a..000000000 --- a/apiserver/plane/app/urls/integration.py +++ /dev/null @@ -1,150 +0,0 @@ -from django.urls import path - - -from plane.app.views import ( - IntegrationViewSet, - WorkspaceIntegrationViewSet, - GithubRepositoriesEndpoint, - GithubRepositorySyncViewSet, - GithubIssueSyncViewSet, - GithubCommentSyncViewSet, - BulkCreateGithubIssueSyncEndpoint, - SlackProjectSyncViewSet, -) - - -urlpatterns = [ - path( - "integrations/", - IntegrationViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="integrations", - ), - path( - "integrations//", - IntegrationViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="integrations", - ), - path( - "workspaces//workspace-integrations/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "list", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//", - WorkspaceIntegrationViewSet.as_view( - { - "post": "create", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//provider/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - name="workspace-integrations", - ), - # Github Integrations - path( - "workspaces//workspace-integrations//github-repositories/", - GithubRepositoriesEndpoint.as_view(), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync/", - GithubRepositorySyncViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync//", - GithubRepositorySyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync/", - GithubIssueSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//bulk-create-github-issue-sync/", - BulkCreateGithubIssueSyncEndpoint.as_view(), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//", - GithubIssueSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync/", - GithubCommentSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync//", - GithubCommentSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - ## End Github Integrations - # Slack Integration - path( - "workspaces//projects//workspace-integrations//project-slack-sync/", - SlackProjectSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//project-slack-sync//", - SlackProjectSyncViewSet.as_view( - { - "delete": "destroy", - "get": "retrieve", - } - ), - ), - ## End Slack Integration -] diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 4ee70450b..0d3b9e063 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -1,30 +1,26 @@ from django.urls import path - from plane.app.views import ( - IssueListEndpoint, - IssueViewSet, - LabelViewSet, BulkCreateIssueLabelsEndpoint, BulkDeleteIssuesEndpoint, - BulkImportIssuesEndpoint, - UserWorkSpaceIssues, SubIssuesEndpoint, IssueLinkViewSet, IssueAttachmentEndpoint, + CommentReactionViewSet, ExportIssuesEndpoint, IssueActivityEndpoint, - IssueCommentViewSet, - IssueSubscriberViewSet, - IssueReactionViewSet, - CommentReactionViewSet, - IssueUserDisplayPropertyEndpoint, IssueArchiveViewSet, - IssueRelationViewSet, + IssueCommentViewSet, IssueDraftViewSet, + IssueListEndpoint, + IssueReactionViewSet, + IssueRelationViewSet, + IssueSubscriberViewSet, + IssueUserDisplayPropertyEndpoint, + IssueViewSet, + LabelViewSet, ) - urlpatterns = [ path( "workspaces//projects//issues/list/", @@ -85,18 +81,7 @@ urlpatterns = [ BulkDeleteIssuesEndpoint.as_view(), name="project-issues-bulk", ), - path( - "workspaces//projects//bulk-import-issues//", - BulkImportIssuesEndpoint.as_view(), - name="project-issues-bulk", - ), - # deprecated endpoint TODO: remove once confirmed - path( - "workspaces//my-issues/", - UserWorkSpaceIssues.as_view(), - name="workspace-issues", - ), - ## + ## path( "workspaces//projects//issues//sub-issues/", SubIssuesEndpoint.as_view(), diff --git a/apiserver/plane/app/urls/module.py b/apiserver/plane/app/urls/module.py index 5e9f4f123..981b4d1fb 100644 --- a/apiserver/plane/app/urls/module.py +++ b/apiserver/plane/app/urls/module.py @@ -6,7 +6,6 @@ from plane.app.views import ( ModuleIssueViewSet, ModuleLinkViewSet, ModuleFavoriteViewSet, - BulkImportModulesEndpoint, ModuleUserPropertiesEndpoint, ) @@ -106,11 +105,6 @@ urlpatterns = [ ), name="user-favorite-module", ), - path( - "workspaces//projects//bulk-import-modules//", - BulkImportModulesEndpoint.as_view(), - name="bulk-modules-create", - ), path( "workspaces//projects//modules//user-properties/", ModuleUserPropertiesEndpoint.as_view(), diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index a70ff18e5..8b21bb9e1 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -22,6 +22,7 @@ from plane.app.views import ( WorkspaceUserPropertiesEndpoint, WorkspaceStatesEndpoint, WorkspaceEstimatesEndpoint, + ExportWorkspaceUserActivityEndpoint, WorkspaceModulesEndpoint, WorkspaceCyclesEndpoint, ) @@ -191,6 +192,11 @@ urlpatterns = [ WorkspaceUserActivityEndpoint.as_view(), name="workspace-user-activity", ), + path( + "workspaces//user-activity//export/", + ExportWorkspaceUserActivityEndpoint.as_view(), + name="export-workspace-user-activity", + ), path( "workspaces//user-profile//", WorkspaceUserProfileEndpoint.as_view(), diff --git a/apiserver/plane/app/urls_deprecated.py b/apiserver/plane/app/urls_deprecated.py deleted file mode 100644 index 2a47285aa..000000000 --- a/apiserver/plane/app/urls_deprecated.py +++ /dev/null @@ -1,1810 +0,0 @@ -from django.urls import path - -from rest_framework_simplejwt.views import TokenRefreshView - -# Create your urls here. - -from plane.app.views import ( - # Authentication - SignUpEndpoint, - SignInEndpoint, - SignOutEndpoint, - MagicSignInEndpoint, - MagicSignInGenerateEndpoint, - OauthEndpoint, - ## End Authentication - # Auth Extended - ForgotPasswordEndpoint, - VerifyEmailEndpoint, - ResetPasswordEndpoint, - RequestEmailVerificationEndpoint, - ChangePasswordEndpoint, - ## End Auth Extender - # User - UserEndpoint, - UpdateUserOnBoardedEndpoint, - UpdateUserTourCompletedEndpoint, - UserActivityEndpoint, - ## End User - # Workspaces - WorkSpaceViewSet, - UserWorkSpacesEndpoint, - InviteWorkspaceEndpoint, - JoinWorkspaceEndpoint, - WorkSpaceMemberViewSet, - WorkspaceMembersEndpoint, - WorkspaceInvitationsViewset, - UserWorkspaceInvitationsEndpoint, - WorkspaceMemberUserEndpoint, - WorkspaceMemberUserViewsEndpoint, - WorkSpaceAvailabilityCheckEndpoint, - TeamMemberViewSet, - AddTeamToProjectEndpoint, - UserLastProjectWithWorkspaceEndpoint, - UserWorkspaceInvitationEndpoint, - UserActivityGraphEndpoint, - UserIssueCompletedGraphEndpoint, - UserWorkspaceDashboardEndpoint, - WorkspaceThemeViewSet, - WorkspaceUserProfileStatsEndpoint, - WorkspaceUserActivityEndpoint, - WorkspaceUserProfileEndpoint, - WorkspaceUserProfileIssuesEndpoint, - WorkspaceLabelsEndpoint, - LeaveWorkspaceEndpoint, - ## End Workspaces - # File Assets - FileAssetEndpoint, - UserAssetsEndpoint, - ## End File Assets - # Projects - ProjectViewSet, - InviteProjectEndpoint, - ProjectMemberViewSet, - ProjectMemberEndpoint, - ProjectMemberInvitationsViewset, - ProjectMemberUserEndpoint, - AddMemberToProjectEndpoint, - ProjectJoinEndpoint, - UserProjectInvitationsViewset, - ProjectIdentifierEndpoint, - ProjectFavoritesViewSet, - LeaveProjectEndpoint, - ProjectPublicCoverImagesEndpoint, - ## End Projects - # Issues - IssueViewSet, - WorkSpaceIssuesEndpoint, - IssueActivityEndpoint, - IssueCommentViewSet, - UserWorkSpaceIssues, - BulkDeleteIssuesEndpoint, - BulkImportIssuesEndpoint, - ProjectUserViewsEndpoint, - IssueUserDisplayPropertyEndpoint, - LabelViewSet, - SubIssuesEndpoint, - IssueLinkViewSet, - BulkCreateIssueLabelsEndpoint, - IssueAttachmentEndpoint, - IssueArchiveViewSet, - IssueSubscriberViewSet, - IssueCommentPublicViewSet, - IssueReactionViewSet, - IssueRelationViewSet, - CommentReactionViewSet, - IssueDraftViewSet, - ## End Issues - # States - StateViewSet, - ## End States - # Estimates - ProjectEstimatePointEndpoint, - BulkEstimatePointEndpoint, - ## End Estimates - # Views - GlobalViewViewSet, - GlobalViewIssuesViewSet, - IssueViewViewSet, - IssueViewFavoriteViewSet, - ## End Views - # Cycles - CycleViewSet, - CycleIssueViewSet, - CycleDateCheckEndpoint, - CycleFavoriteViewSet, - TransferCycleIssueEndpoint, - ## End Cycles - # Modules - ModuleViewSet, - ModuleIssueViewSet, - ModuleFavoriteViewSet, - ModuleLinkViewSet, - BulkImportModulesEndpoint, - ## End Modules - # Pages - PageViewSet, - PageLogEndpoint, - SubPagesEndpoint, - PageFavoriteViewSet, - CreateIssueFromBlockEndpoint, - ## End Pages - # Api Tokens - ApiTokenEndpoint, - ## End Api Tokens - # Integrations - IntegrationViewSet, - WorkspaceIntegrationViewSet, - GithubRepositoriesEndpoint, - GithubRepositorySyncViewSet, - GithubIssueSyncViewSet, - GithubCommentSyncViewSet, - BulkCreateGithubIssueSyncEndpoint, - SlackProjectSyncViewSet, - ## End Integrations - # Importer - ServiceIssueImportSummaryEndpoint, - ImportServiceEndpoint, - UpdateServiceImportStatusEndpoint, - ## End importer - # Search - GlobalSearchEndpoint, - IssueSearchEndpoint, - ## End Search - # External - GPTIntegrationEndpoint, - ReleaseNotesEndpoint, - UnsplashEndpoint, - ## End External - # Inbox - InboxViewSet, - InboxIssueViewSet, - ## End Inbox - # Analytics - AnalyticsEndpoint, - AnalyticViewViewset, - SavedAnalyticEndpoint, - ExportAnalyticsEndpoint, - DefaultAnalyticsEndpoint, - ## End Analytics - # Notification - NotificationViewSet, - UnreadNotificationEndpoint, - MarkAllReadNotificationViewSet, - ## End Notification - # Public Boards - ProjectDeployBoardViewSet, - ProjectIssuesPublicEndpoint, - ProjectDeployBoardPublicSettingsEndpoint, - IssueReactionPublicViewSet, - CommentReactionPublicViewSet, - InboxIssuePublicViewSet, - IssueVotePublicViewSet, - WorkspaceProjectDeployBoardEndpoint, - IssueRetrievePublicEndpoint, - ## End Public Boards - ## Exporter - ExportIssuesEndpoint, - ## End Exporter - # Configuration - ConfigurationEndpoint, - ## End Configuration -) - - -# TODO: Delete this file -# This url file has been deprecated use apiserver/plane/urls folder to create new urls - -urlpatterns = [ - # Social Auth - path("social-auth/", OauthEndpoint.as_view(), name="oauth"), - # Auth - path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"), - path("sign-in/", SignInEndpoint.as_view(), name="sign-in"), - path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), - # Magic Sign In/Up - path( - "magic-generate/", - MagicSignInGenerateEndpoint.as_view(), - name="magic-generate", - ), - path( - "magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in" - ), - path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), - # Email verification - path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"), - path( - "request-email-verify/", - RequestEmailVerificationEndpoint.as_view(), - name="request-reset-email", - ), - # Password Manipulation - path( - "reset-password///", - ResetPasswordEndpoint.as_view(), - name="password-reset", - ), - path( - "forgot-password/", - ForgotPasswordEndpoint.as_view(), - name="forgot-password", - ), - # User Profile - path( - "users/me/", - UserEndpoint.as_view( - {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} - ), - name="users", - ), - path( - "users/me/settings/", - UserEndpoint.as_view( - { - "get": "retrieve_user_settings", - } - ), - name="users", - ), - path( - "users/me/change-password/", - ChangePasswordEndpoint.as_view(), - name="change-password", - ), - path( - "users/me/onboard/", - UpdateUserOnBoardedEndpoint.as_view(), - name="user-onboard", - ), - path( - "users/me/tour-completed/", - UpdateUserTourCompletedEndpoint.as_view(), - name="user-tour", - ), - path( - "users/workspaces//activities/", - UserActivityEndpoint.as_view(), - name="user-activities", - ), - # user workspaces - path( - "users/me/workspaces/", - UserWorkSpacesEndpoint.as_view(), - name="user-workspace", - ), - # user workspace invitations - path( - "users/me/invitations/workspaces/", - UserWorkspaceInvitationsEndpoint.as_view( - {"get": "list", "post": "create"} - ), - name="user-workspace-invitations", - ), - # user workspace invitation - path( - "users/me/invitations//", - UserWorkspaceInvitationEndpoint.as_view( - { - "get": "retrieve", - } - ), - name="workspace", - ), - # user join workspace - # User Graphs - path( - "users/me/workspaces//activity-graph/", - UserActivityGraphEndpoint.as_view(), - name="user-activity-graph", - ), - path( - "users/me/workspaces//issues-completed-graph/", - UserIssueCompletedGraphEndpoint.as_view(), - name="completed-graph", - ), - path( - "users/me/workspaces//dashboard/", - UserWorkspaceDashboardEndpoint.as_view(), - name="user-workspace-dashboard", - ), - ## User Graph - path( - "users/me/invitations/workspaces///join/", - JoinWorkspaceEndpoint.as_view(), - name="user-join-workspace", - ), - # user project invitations - path( - "users/me/invitations/projects/", - UserProjectInvitationsViewset.as_view( - {"get": "list", "post": "create"} - ), - name="user-project-invitaions", - ), - ## Workspaces ## - path( - "workspace-slug-check/", - WorkSpaceAvailabilityCheckEndpoint.as_view(), - name="workspace-availability", - ), - path( - "workspaces/", - WorkSpaceViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="workspace", - ), - path( - "workspaces//", - WorkSpaceViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="workspace", - ), - path( - "workspaces//invite/", - InviteWorkspaceEndpoint.as_view(), - name="workspace", - ), - path( - "workspaces//invitations/", - WorkspaceInvitationsViewset.as_view({"get": "list"}), - name="workspace", - ), - path( - "workspaces//invitations//", - WorkspaceInvitationsViewset.as_view( - { - "delete": "destroy", - "get": "retrieve", - } - ), - name="workspace", - ), - path( - "workspaces//members/", - WorkSpaceMemberViewSet.as_view({"get": "list"}), - name="workspace", - ), - path( - "workspaces//members//", - WorkSpaceMemberViewSet.as_view( - { - "patch": "partial_update", - "delete": "destroy", - "get": "retrieve", - } - ), - name="workspace", - ), - path( - "workspaces//workspace-members/", - WorkspaceMembersEndpoint.as_view(), - name="workspace-members", - ), - path( - "workspaces//teams/", - TeamMemberViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="workspace", - ), - path( - "workspaces//teams//", - TeamMemberViewSet.as_view( - { - "put": "update", - "patch": "partial_update", - "delete": "destroy", - "get": "retrieve", - } - ), - name="workspace", - ), - path( - "users/last-visited-workspace/", - UserLastProjectWithWorkspaceEndpoint.as_view(), - name="workspace-project-details", - ), - path( - "workspaces//workspace-members/me/", - WorkspaceMemberUserEndpoint.as_view(), - name="workspace-member-details", - ), - path( - "workspaces//workspace-views/", - WorkspaceMemberUserViewsEndpoint.as_view(), - name="workspace-member-details", - ), - path( - "workspaces//workspace-themes/", - WorkspaceThemeViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="workspace-themes", - ), - path( - "workspaces//workspace-themes//", - WorkspaceThemeViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="workspace-themes", - ), - path( - "workspaces//user-stats//", - WorkspaceUserProfileStatsEndpoint.as_view(), - name="workspace-user-stats", - ), - path( - "workspaces//user-activity//", - WorkspaceUserActivityEndpoint.as_view(), - name="workspace-user-activity", - ), - path( - "workspaces//user-profile//", - WorkspaceUserProfileEndpoint.as_view(), - name="workspace-user-profile-page", - ), - path( - "workspaces//user-issues//", - WorkspaceUserProfileIssuesEndpoint.as_view(), - name="workspace-user-profile-issues", - ), - path( - "workspaces//labels/", - WorkspaceLabelsEndpoint.as_view(), - name="workspace-labels", - ), - path( - "workspaces//members/leave/", - LeaveWorkspaceEndpoint.as_view(), - name="workspace-labels", - ), - ## End Workspaces ## - # Projects - path( - "workspaces//projects/", - ProjectViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project", - ), - path( - "workspaces//projects//", - ProjectViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project", - ), - path( - "workspaces//project-identifiers/", - ProjectIdentifierEndpoint.as_view(), - name="project-identifiers", - ), - path( - "workspaces//projects//invite/", - InviteProjectEndpoint.as_view(), - name="project", - ), - path( - "workspaces//projects//members/", - ProjectMemberViewSet.as_view({"get": "list"}), - name="project", - ), - path( - "workspaces//projects//members//", - ProjectMemberViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project", - ), - path( - "workspaces//projects//project-members/", - ProjectMemberEndpoint.as_view(), - name="project", - ), - path( - "workspaces//projects//members/add/", - AddMemberToProjectEndpoint.as_view(), - name="project", - ), - path( - "workspaces//projects/join/", - ProjectJoinEndpoint.as_view(), - name="project", - ), - path( - "workspaces//projects//team-invite/", - AddTeamToProjectEndpoint.as_view(), - name="projects", - ), - path( - "workspaces//projects//invitations/", - ProjectMemberInvitationsViewset.as_view({"get": "list"}), - name="workspace", - ), - path( - "workspaces//projects//invitations//", - ProjectMemberInvitationsViewset.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - name="project", - ), - path( - "workspaces//projects//project-views/", - ProjectUserViewsEndpoint.as_view(), - name="project-view", - ), - path( - "workspaces//projects//project-members/me/", - ProjectMemberUserEndpoint.as_view(), - name="project-view", - ), - path( - "workspaces//user-favorite-projects/", - ProjectFavoritesViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project", - ), - path( - "workspaces//user-favorite-projects//", - ProjectFavoritesViewSet.as_view( - { - "delete": "destroy", - } - ), - name="project", - ), - path( - "workspaces//projects//members/leave/", - LeaveProjectEndpoint.as_view(), - name="project", - ), - path( - "project-covers/", - ProjectPublicCoverImagesEndpoint.as_view(), - name="project-covers", - ), - # End Projects - # States - path( - "workspaces//projects//states/", - StateViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-states", - ), - path( - "workspaces//projects//states//", - StateViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-state", - ), - # End States ## - # Estimates - path( - "workspaces//projects//project-estimates/", - ProjectEstimatePointEndpoint.as_view(), - name="project-estimate-points", - ), - path( - "workspaces//projects//estimates/", - BulkEstimatePointEndpoint.as_view( - { - "get": "list", - "post": "create", - } - ), - name="bulk-create-estimate-points", - ), - path( - "workspaces//projects//estimates//", - BulkEstimatePointEndpoint.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="bulk-create-estimate-points", - ), - # End Estimates ## - # Views - path( - "workspaces//projects//views/", - IssueViewViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-view", - ), - path( - "workspaces//projects//views//", - IssueViewViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-view", - ), - path( - "workspaces//views/", - GlobalViewViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="global-view", - ), - path( - "workspaces//views//", - GlobalViewViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="global-view", - ), - path( - "workspaces//issues/", - GlobalViewIssuesViewSet.as_view( - { - "get": "list", - } - ), - name="global-view-issues", - ), - path( - "workspaces//projects//user-favorite-views/", - IssueViewFavoriteViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="user-favorite-view", - ), - path( - "workspaces//projects//user-favorite-views//", - IssueViewFavoriteViewSet.as_view( - { - "delete": "destroy", - } - ), - name="user-favorite-view", - ), - ## End Views - ## Cycles - path( - "workspaces//projects//cycles/", - CycleViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-cycle", - ), - path( - "workspaces//projects//cycles//", - CycleViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-cycle", - ), - path( - "workspaces//projects//cycles//cycle-issues/", - CycleIssueViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-cycle", - ), - path( - "workspaces//projects//cycles//cycle-issues//", - CycleIssueViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-cycle", - ), - path( - "workspaces//projects//cycles/date-check/", - CycleDateCheckEndpoint.as_view(), - name="project-cycle", - ), - path( - "workspaces//projects//user-favorite-cycles/", - CycleFavoriteViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="user-favorite-cycle", - ), - path( - "workspaces//projects//user-favorite-cycles//", - CycleFavoriteViewSet.as_view( - { - "delete": "destroy", - } - ), - name="user-favorite-cycle", - ), - path( - "workspaces//projects//cycles//transfer-issues/", - TransferCycleIssueEndpoint.as_view(), - name="transfer-issues", - ), - ## End Cycles - # Issue - path( - "workspaces//projects//issues/", - IssueViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue", - ), - path( - "workspaces//projects//issues//", - IssueViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue", - ), - path( - "workspaces//projects//issue-labels/", - LabelViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-labels", - ), - path( - "workspaces//projects//issue-labels//", - LabelViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-labels", - ), - path( - "workspaces//projects//bulk-create-labels/", - BulkCreateIssueLabelsEndpoint.as_view(), - name="project-bulk-labels", - ), - path( - "workspaces//projects//bulk-delete-issues/", - BulkDeleteIssuesEndpoint.as_view(), - name="project-issues-bulk", - ), - path( - "workspaces//projects//bulk-import-issues//", - BulkImportIssuesEndpoint.as_view(), - name="project-issues-bulk", - ), - path( - "workspaces//my-issues/", - UserWorkSpaceIssues.as_view(), - name="workspace-issues", - ), - path( - "workspaces//projects//issues//sub-issues/", - SubIssuesEndpoint.as_view(), - name="sub-issues", - ), - path( - "workspaces//projects//issues//issue-links/", - IssueLinkViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-links", - ), - path( - "workspaces//projects//issues//issue-links//", - IssueLinkViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-links", - ), - path( - "workspaces//projects//issues//issue-attachments/", - IssueAttachmentEndpoint.as_view(), - name="project-issue-attachments", - ), - path( - "workspaces//projects//issues//issue-attachments//", - IssueAttachmentEndpoint.as_view(), - name="project-issue-attachments", - ), - path( - "workspaces//export-issues/", - ExportIssuesEndpoint.as_view(), - name="export-issues", - ), - ## End Issues - ## Issue Activity - path( - "workspaces//projects//issues//history/", - IssueActivityEndpoint.as_view(), - name="project-issue-history", - ), - ## Issue Activity - ## IssueComments - path( - "workspaces//projects//issues//comments/", - IssueCommentViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-comment", - ), - path( - "workspaces//projects//issues//comments//", - IssueCommentViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-comment", - ), - ## End IssueComments - # Issue Subscribers - path( - "workspaces//projects//issues//issue-subscribers/", - IssueSubscriberViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-subscribers", - ), - path( - "workspaces//projects//issues//issue-subscribers//", - IssueSubscriberViewSet.as_view({"delete": "destroy"}), - name="project-issue-subscribers", - ), - path( - "workspaces//projects//issues//subscribe/", - IssueSubscriberViewSet.as_view( - { - "get": "subscription_status", - "post": "subscribe", - "delete": "unsubscribe", - } - ), - name="project-issue-subscribers", - ), - ## End Issue Subscribers - # Issue Reactions - path( - "workspaces//projects//issues//reactions/", - IssueReactionViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-reactions", - ), - path( - "workspaces//projects//issues//reactions//", - IssueReactionViewSet.as_view( - { - "delete": "destroy", - } - ), - name="project-issue-reactions", - ), - ## End Issue Reactions - # Comment Reactions - path( - "workspaces//projects//comments//reactions/", - CommentReactionViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-comment-reactions", - ), - path( - "workspaces//projects//comments//reactions//", - CommentReactionViewSet.as_view( - { - "delete": "destroy", - } - ), - name="project-issue-comment-reactions", - ), - ## End Comment Reactions - ## IssueProperty - path( - "workspaces//projects//issue-display-properties/", - IssueUserDisplayPropertyEndpoint.as_view(), - name="project-issue-display-properties", - ), - ## IssueProperty Ebd - ## Issue Archives - path( - "workspaces//projects//archived-issues/", - IssueArchiveViewSet.as_view( - { - "get": "list", - } - ), - name="project-issue-archive", - ), - path( - "workspaces//projects//archived-issues//", - IssueArchiveViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - name="project-issue-archive", - ), - path( - "workspaces//projects//unarchive//", - IssueArchiveViewSet.as_view( - { - "post": "unarchive", - } - ), - name="project-issue-archive", - ), - ## End Issue Archives - ## Issue Relation - path( - "workspaces//projects//issues//issue-relation/", - IssueRelationViewSet.as_view( - { - "post": "create", - } - ), - name="issue-relation", - ), - path( - "workspaces//projects//issues//issue-relation//", - IssueRelationViewSet.as_view( - { - "delete": "destroy", - } - ), - name="issue-relation", - ), - ## End Issue Relation - ## Issue Drafts - path( - "workspaces//projects//issue-drafts/", - IssueDraftViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-draft", - ), - path( - "workspaces//projects//issue-drafts//", - IssueDraftViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-draft", - ), - ## End Issue Drafts - ## File Assets - path( - "workspaces//file-assets/", - FileAssetEndpoint.as_view(), - name="file-assets", - ), - path( - "workspaces/file-assets///", - FileAssetEndpoint.as_view(), - name="file-assets", - ), - path( - "users/file-assets/", - UserAssetsEndpoint.as_view(), - name="user-file-assets", - ), - path( - "users/file-assets//", - UserAssetsEndpoint.as_view(), - name="user-file-assets", - ), - ## End File Assets - ## Modules - path( - "workspaces//projects//modules/", - ModuleViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-modules", - ), - path( - "workspaces//projects//modules//", - ModuleViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-modules", - ), - path( - "workspaces//projects//modules//module-issues/", - ModuleIssueViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-module-issues", - ), - path( - "workspaces//projects//modules//module-issues//", - ModuleIssueViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-module-issues", - ), - path( - "workspaces//projects//modules//module-links/", - ModuleLinkViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-module-links", - ), - path( - "workspaces//projects//modules//module-links//", - ModuleLinkViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-module-links", - ), - path( - "workspaces//projects//user-favorite-modules/", - ModuleFavoriteViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="user-favorite-module", - ), - path( - "workspaces//projects//user-favorite-modules//", - ModuleFavoriteViewSet.as_view( - { - "delete": "destroy", - } - ), - name="user-favorite-module", - ), - path( - "workspaces//projects//bulk-import-modules//", - BulkImportModulesEndpoint.as_view(), - name="bulk-modules-create", - ), - ## End Modules - # Pages - path( - "workspaces//projects//pages/", - PageViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-pages", - ), - path( - "workspaces//projects//pages//", - PageViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-pages", - ), - path( - "workspaces//projects//pages//archive/", - PageViewSet.as_view( - { - "post": "archive", - } - ), - name="project-page-archive", - ), - path( - "workspaces//projects//pages//unarchive/", - PageViewSet.as_view( - { - "post": "unarchive", - } - ), - name="project-page-unarchive", - ), - path( - "workspaces//projects//archived-pages/", - PageViewSet.as_view( - { - "get": "archive_list", - } - ), - name="project-pages", - ), - path( - "workspaces//projects//pages//lock/", - PageViewSet.as_view( - { - "post": "lock", - } - ), - name="project-pages", - ), - path( - "workspaces//projects//pages//unlock/", - PageViewSet.as_view( - { - "post": "unlock", - } - ), - ), - path( - "workspaces//projects//pages//transactions/", - PageLogEndpoint.as_view(), - name="page-transactions", - ), - path( - "workspaces//projects//pages//transactions//", - PageLogEndpoint.as_view(), - name="page-transactions", - ), - path( - "workspaces//projects//pages//sub-pages/", - SubPagesEndpoint.as_view(), - name="sub-page", - ), - path( - "workspaces//projects//estimates/", - BulkEstimatePointEndpoint.as_view( - { - "get": "list", - "post": "create", - } - ), - name="bulk-create-estimate-points", - ), - path( - "workspaces//projects//estimates//", - BulkEstimatePointEndpoint.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="bulk-create-estimate-points", - ), - path( - "workspaces//projects//user-favorite-pages/", - PageFavoriteViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="user-favorite-pages", - ), - path( - "workspaces//projects//user-favorite-pages//", - PageFavoriteViewSet.as_view( - { - "delete": "destroy", - } - ), - name="user-favorite-pages", - ), - path( - "workspaces//projects//pages//page-blocks//issues/", - CreateIssueFromBlockEndpoint.as_view(), - name="page-block-issues", - ), - ## End Pages - # API Tokens - path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), - path( - "api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens" - ), - ## End API Tokens - # Integrations - path( - "integrations/", - IntegrationViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="integrations", - ), - path( - "integrations//", - IntegrationViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="integrations", - ), - path( - "workspaces//workspace-integrations/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "list", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//", - WorkspaceIntegrationViewSet.as_view( - { - "post": "create", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//provider/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - name="workspace-integrations", - ), - # Github Integrations - path( - "workspaces//workspace-integrations//github-repositories/", - GithubRepositoriesEndpoint.as_view(), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync/", - GithubRepositorySyncViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync//", - GithubRepositorySyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync/", - GithubIssueSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//bulk-create-github-issue-sync/", - BulkCreateGithubIssueSyncEndpoint.as_view(), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//", - GithubIssueSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync/", - GithubCommentSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync//", - GithubCommentSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - ## End Github Integrations - # Slack Integration - path( - "workspaces//projects//workspace-integrations//project-slack-sync/", - SlackProjectSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//project-slack-sync//", - SlackProjectSyncViewSet.as_view( - { - "delete": "destroy", - "get": "retrieve", - } - ), - ), - ## End Slack Integration - ## End Integrations - # Importer - path( - "workspaces//importers//", - ServiceIssueImportSummaryEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//projects/importers//", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers/", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers///", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//projects//service//importers//", - UpdateServiceImportStatusEndpoint.as_view(), - name="importer", - ), - ## End Importer - # Search - path( - "workspaces//search/", - GlobalSearchEndpoint.as_view(), - name="global-search", - ), - path( - "workspaces//projects//search-issues/", - IssueSearchEndpoint.as_view(), - name="project-issue-search", - ), - ## End Search - # External - path( - "workspaces//projects//ai-assistant/", - GPTIntegrationEndpoint.as_view(), - name="importer", - ), - path( - "release-notes/", - ReleaseNotesEndpoint.as_view(), - name="release-notes", - ), - path( - "unsplash/", - UnsplashEndpoint.as_view(), - name="release-notes", - ), - ## End External - # Inbox - path( - "workspaces//projects//inboxes/", - InboxViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="inbox", - ), - path( - "workspaces//projects//inboxes//", - InboxViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="inbox", - ), - path( - "workspaces//projects//inboxes//inbox-issues/", - InboxIssueViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="inbox-issue", - ), - path( - "workspaces//projects//inboxes//inbox-issues//", - InboxIssueViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="inbox-issue", - ), - ## End Inbox - # Analytics - path( - "workspaces//analytics/", - AnalyticsEndpoint.as_view(), - name="plane-analytics", - ), - path( - "workspaces//analytic-view/", - AnalyticViewViewset.as_view({"get": "list", "post": "create"}), - name="analytic-view", - ), - path( - "workspaces//analytic-view//", - AnalyticViewViewset.as_view( - {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} - ), - name="analytic-view", - ), - path( - "workspaces//saved-analytic-view//", - SavedAnalyticEndpoint.as_view(), - name="saved-analytic-view", - ), - path( - "workspaces//export-analytics/", - ExportAnalyticsEndpoint.as_view(), - name="export-analytics", - ), - path( - "workspaces//default-analytics/", - DefaultAnalyticsEndpoint.as_view(), - name="default-analytics", - ), - ## End Analytics - # Notification - path( - "workspaces//users/notifications/", - NotificationViewSet.as_view( - { - "get": "list", - } - ), - name="notifications", - ), - path( - "workspaces//users/notifications//", - NotificationViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="notifications", - ), - path( - "workspaces//users/notifications//read/", - NotificationViewSet.as_view( - { - "post": "mark_read", - "delete": "mark_unread", - } - ), - name="notifications", - ), - path( - "workspaces//users/notifications//archive/", - NotificationViewSet.as_view( - { - "post": "archive", - "delete": "unarchive", - } - ), - name="notifications", - ), - path( - "workspaces//users/notifications/unread/", - 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( - "workspaces//projects//project-deploy-boards/", - ProjectDeployBoardViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-deploy-board", - ), - path( - "workspaces//projects//project-deploy-boards//", - ProjectDeployBoardViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-deploy-board", - ), - path( - "public/workspaces//project-boards//settings/", - ProjectDeployBoardPublicSettingsEndpoint.as_view(), - name="project-deploy-board-settings", - ), - path( - "public/workspaces//project-boards//issues/", - 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( - { - "get": "list", - "post": "create", - } - ), - name="issue-comments-project-board", - ), - path( - "public/workspaces//project-boards//issues//comments//", - IssueCommentPublicViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="issue-comments-project-board", - ), - path( - "public/workspaces//project-boards//issues//reactions/", - IssueReactionPublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="issue-reactions-project-board", - ), - path( - "public/workspaces//project-boards//issues//reactions//", - IssueReactionPublicViewSet.as_view( - { - "delete": "destroy", - } - ), - name="issue-reactions-project-board", - ), - path( - "public/workspaces//project-boards//comments//reactions/", - CommentReactionPublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="comment-reactions-project-board", - ), - path( - "public/workspaces//project-boards//comments//reactions//", - CommentReactionPublicViewSet.as_view( - { - "delete": "destroy", - } - ), - name="comment-reactions-project-board", - ), - path( - "public/workspaces//project-boards//inboxes//inbox-issues/", - InboxIssuePublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="inbox-issue", - ), - path( - "public/workspaces//project-boards//inboxes//inbox-issues//", - InboxIssuePublicViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="inbox-issue", - ), - path( - "public/workspaces//project-boards//issues//votes/", - IssueVotePublicViewSet.as_view( - { - "get": "list", - "post": "create", - "delete": "destroy", - } - ), - name="issue-vote-project-board", - ), - path( - "public/workspaces//project-boards/", - WorkspaceProjectDeployBoardEndpoint.as_view(), - name="workspace-project-boards", - ), - ## End Public Boards - # Configuration - path( - "configs/", - ConfigurationEndpoint.as_view(), - name="configuration", - ), - ## End Configuration -] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index d4a13e497..bb5b7dd74 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -1,19 +1,26 @@ -from .project import ( +from .project.base import ( ProjectViewSet, - ProjectMemberViewSet, - UserProjectInvitationsViewset, - ProjectInvitationsViewset, - AddTeamToProjectEndpoint, ProjectIdentifierEndpoint, - ProjectJoinEndpoint, ProjectUserViewsEndpoint, - ProjectMemberUserEndpoint, ProjectFavoritesViewSet, ProjectPublicCoverImagesEndpoint, ProjectDeployBoardViewSet, +) + +from .project.invite import ( + UserProjectInvitationsViewset, + ProjectInvitationsViewset, + ProjectJoinEndpoint, +) + +from .project.member import ( + ProjectMemberViewSet, + AddTeamToProjectEndpoint, + ProjectMemberUserEndpoint, UserProjectRolesEndpoint, ) -from .user import ( + +from .user.base import ( UserEndpoint, UpdateUserOnBoardedEndpoint, UpdateUserTourCompletedEndpoint, @@ -24,70 +31,121 @@ from .oauth import OauthEndpoint from .base import BaseAPIView, BaseViewSet, WebhookMixin -from .workspace import ( +from .workspace.base import ( WorkSpaceViewSet, UserWorkSpacesEndpoint, WorkSpaceAvailabilityCheckEndpoint, - WorkspaceJoinEndpoint, - WorkSpaceMemberViewSet, - TeamMemberViewSet, - WorkspaceInvitationsViewset, - UserWorkspaceInvitationsViewSet, - UserLastProjectWithWorkspaceEndpoint, - WorkspaceMemberUserEndpoint, - WorkspaceMemberUserViewsEndpoint, - UserActivityGraphEndpoint, - UserIssueCompletedGraphEndpoint, UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, - WorkspaceUserProfileStatsEndpoint, - WorkspaceUserActivityEndpoint, - WorkspaceUserProfileEndpoint, - WorkspaceUserProfileIssuesEndpoint, - WorkspaceLabelsEndpoint, + ExportWorkspaceUserActivityEndpoint +) + +from .workspace.member import ( + WorkSpaceMemberViewSet, + TeamMemberViewSet, + WorkspaceMemberUserEndpoint, WorkspaceProjectMemberEndpoint, - WorkspaceUserPropertiesEndpoint, + WorkspaceMemberUserViewsEndpoint, +) +from .workspace.invite import ( + WorkspaceInvitationsViewset, + WorkspaceJoinEndpoint, + UserWorkspaceInvitationsViewSet, +) +from .workspace.label import ( + WorkspaceLabelsEndpoint, +) +from .workspace.state import ( WorkspaceStatesEndpoint, +) +from .workspace.user import ( + UserLastProjectWithWorkspaceEndpoint, + WorkspaceUserProfileIssuesEndpoint, + WorkspaceUserPropertiesEndpoint, + WorkspaceUserProfileEndpoint, + WorkspaceUserActivityEndpoint, + WorkspaceUserProfileStatsEndpoint, + UserActivityGraphEndpoint, + UserIssueCompletedGraphEndpoint, +) +from .workspace.estimate import ( WorkspaceEstimatesEndpoint, +) +from .workspace.module import ( WorkspaceModulesEndpoint, +) +from .workspace.cycle import ( WorkspaceCyclesEndpoint, ) -from .state import StateViewSet -from .view import ( + +from .state.base import StateViewSet +from .view.base import ( GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet, ) -from .cycle import ( +from .cycle.base import ( CycleViewSet, - CycleIssueViewSet, CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, CycleUserPropertiesEndpoint, ) -from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet -from .issue import ( +from .cycle.issue import ( + CycleIssueViewSet, +) + +from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet +from .issue.base import ( IssueListEndpoint, IssueViewSet, - WorkSpaceIssuesEndpoint, - IssueActivityEndpoint, - IssueCommentViewSet, IssueUserDisplayPropertyEndpoint, - LabelViewSet, BulkDeleteIssuesEndpoint, - UserWorkSpaceIssues, - SubIssuesEndpoint, - IssueLinkViewSet, - BulkCreateIssueLabelsEndpoint, - IssueAttachmentEndpoint, +) + +from .issue.activity import ( + IssueActivityEndpoint, +) + +from .issue.archive import ( IssueArchiveViewSet, - IssueSubscriberViewSet, +) + +from .issue.attachment import ( + IssueAttachmentEndpoint, +) + +from .issue.comment import ( + IssueCommentViewSet, CommentReactionViewSet, - IssueReactionViewSet, +) + +from .issue.draft import IssueDraftViewSet + +from .issue.label import ( + LabelViewSet, + BulkCreateIssueLabelsEndpoint, +) + +from .issue.link import ( + IssueLinkViewSet, +) + +from .issue.relation import ( IssueRelationViewSet, - IssueDraftViewSet, +) + +from .issue.reaction import ( + IssueReactionViewSet, +) + +from .issue.sub_issue import ( + SubIssuesEndpoint, +) + +from .issue.subscriber import ( + IssueSubscriberViewSet, ) from .auth_extended import ( @@ -106,36 +164,21 @@ from .authentication import ( MagicSignInEndpoint, ) -from .module import ( +from .module.base import ( ModuleViewSet, - ModuleIssueViewSet, ModuleLinkViewSet, ModuleFavoriteViewSet, ModuleUserPropertiesEndpoint, ) +from .module.issue import ( + ModuleIssueViewSet, +) + from .api import ApiTokenEndpoint -from .integration import ( - WorkspaceIntegrationViewSet, - IntegrationViewSet, - GithubIssueSyncViewSet, - GithubRepositorySyncViewSet, - GithubCommentSyncViewSet, - GithubRepositoriesEndpoint, - BulkCreateGithubIssueSyncEndpoint, - SlackProjectSyncViewSet, -) -from .importer import ( - ServiceIssueImportSummaryEndpoint, - ImportServiceEndpoint, - UpdateServiceImportStatusEndpoint, - BulkImportIssuesEndpoint, - BulkImportModulesEndpoint, -) - -from .page import ( +from .page.base import ( PageViewSet, PageFavoriteViewSet, PageLogEndpoint, @@ -145,20 +188,19 @@ from .page import ( from .search import GlobalSearchEndpoint, IssueSearchEndpoint -from .external import ( +from .external.base import ( GPTIntegrationEndpoint, - ReleaseNotesEndpoint, UnsplashEndpoint, ) -from .estimate import ( +from .estimate.base import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, ) -from .inbox import InboxViewSet, InboxIssueViewSet +from .inbox.base import InboxViewSet, InboxIssueViewSet -from .analytic import ( +from .analytic.base import ( AnalyticsEndpoint, AnalyticViewViewset, SavedAnalyticEndpoint, @@ -166,24 +208,23 @@ from .analytic import ( DefaultAnalyticsEndpoint, ) -from .notification import ( +from .notification.base import ( NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet, UserNotificationPreferenceEndpoint, ) -from .exporter import ExportIssuesEndpoint +from .exporter.base import ExportIssuesEndpoint from .config import ConfigurationEndpoint, MobileConfigurationEndpoint -from .webhook import ( +from .webhook.base import ( WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint, ) -from .dashboard import ( - DashboardEndpoint, - WidgetsEndpoint -) \ No newline at end of file +from .dashboard.base import DashboardEndpoint, WidgetsEndpoint + +from .error_404 import custom_404_view diff --git a/apiserver/plane/app/views/analytic.py b/apiserver/plane/app/views/analytic/base.py similarity index 97% rename from apiserver/plane/app/views/analytic.py rename to apiserver/plane/app/views/analytic/base.py index 6eb914b23..8e0d3220d 100644 --- a/apiserver/plane/app/views/analytic.py +++ b/apiserver/plane/app/views/analytic/base.py @@ -1,5 +1,5 @@ # Django imports -from django.db.models import Count, Sum, F, Q +from django.db.models import Count, Sum, F from django.db.models.functions import ExtractMonth from django.utils import timezone @@ -10,7 +10,7 @@ from rest_framework.response import Response # Module imports from plane.app.views import BaseAPIView, BaseViewSet from plane.app.permissions import WorkSpaceAdminPermission -from plane.db.models import Issue, AnalyticView, Workspace, State, Label +from plane.db.models import Issue, AnalyticView, Workspace from plane.app.serializers import AnalyticViewSerializer from plane.utils.analytics_plot import build_graph_plot from plane.bgtasks.analytic_plot_export import analytic_export_task @@ -51,8 +51,8 @@ class AnalyticsEndpoint(BaseAPIView): if ( not x_axis or not y_axis - or not x_axis in valid_xaxis_segment - or not y_axis in valid_yaxis + or x_axis not in valid_xaxis_segment + or y_axis not in valid_yaxis ): return Response( { @@ -266,8 +266,8 @@ class ExportAnalyticsEndpoint(BaseAPIView): if ( not x_axis or not y_axis - or not x_axis in valid_xaxis_segment - or not y_axis in valid_yaxis + or x_axis not in valid_xaxis_segment + or y_axis not in valid_yaxis ): return Response( { diff --git a/apiserver/plane/app/views/api.py b/apiserver/plane/app/views/api.py index 86a29c7fa..6cd349b07 100644 --- a/apiserver/plane/app/views/api.py +++ b/apiserver/plane/app/views/api.py @@ -43,7 +43,7 @@ class ApiTokenEndpoint(BaseAPIView): ) def get(self, request, slug, pk=None): - if pk == None: + if pk is None: api_tokens = APIToken.objects.filter( user=request.user, workspace__slug=slug ) diff --git a/apiserver/plane/app/views/asset.py b/apiserver/plane/app/views/asset/base.py similarity index 98% rename from apiserver/plane/app/views/asset.py rename to apiserver/plane/app/views/asset/base.py index fb5590610..6de4a4ee7 100644 --- a/apiserver/plane/app/views/asset.py +++ b/apiserver/plane/app/views/asset/base.py @@ -4,7 +4,7 @@ from rest_framework.response import Response from rest_framework.parsers import MultiPartParser, FormParser, JSONParser # Module imports -from .base import BaseAPIView, BaseViewSet +from ..base import BaseAPIView, BaseViewSet from plane.db.models import FileAsset, Workspace from plane.app.serializers import FileAssetSerializer diff --git a/apiserver/plane/app/views/auth_extended.py b/apiserver/plane/app/views/auth_extended.py index 29cb43e38..896f4170f 100644 --- a/apiserver/plane/app/views/auth_extended.py +++ b/apiserver/plane/app/views/auth_extended.py @@ -16,7 +16,6 @@ from django.contrib.auth.hashers import make_password from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.core.validators import validate_email from django.core.exceptions import ValidationError -from django.conf import settings ## Third Party Imports from rest_framework import status @@ -172,7 +171,7 @@ class ResetPasswordEndpoint(BaseAPIView): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - except DjangoUnicodeDecodeError as indentifier: + except DjangoUnicodeDecodeError: return Response( {"error": "token is not valid, please check the new one"}, status=status.HTTP_401_UNAUTHORIZED, diff --git a/apiserver/plane/app/views/authentication.py b/apiserver/plane/app/views/authentication.py index c2b3e0b7e..7d898f971 100644 --- a/apiserver/plane/app/views/authentication.py +++ b/apiserver/plane/app/views/authentication.py @@ -7,7 +7,6 @@ import json from django.utils import timezone from django.core.exceptions import ValidationError from django.core.validators import validate_email -from django.conf import settings from django.contrib.auth.hashers import make_password # Third party imports @@ -65,7 +64,7 @@ class SignUpEndpoint(BaseAPIView): email = email.strip().lower() try: validate_email(email) - except ValidationError as e: + except ValidationError: return Response( {"error": "Please provide a valid email address."}, status=status.HTTP_400_BAD_REQUEST, @@ -151,7 +150,7 @@ class SignInEndpoint(BaseAPIView): email = email.strip().lower() try: validate_email(email) - except ValidationError as e: + except ValidationError: return Response( {"error": "Please provide a valid email address."}, status=status.HTTP_400_BAD_REQUEST, @@ -238,9 +237,11 @@ class SignInEndpoint(BaseAPIView): [ WorkspaceMember( workspace_id=project_member_invite.workspace_id, - role=project_member_invite.role - if project_member_invite.role in [5, 10, 15] - else 15, + role=( + project_member_invite.role + if project_member_invite.role in [5, 10, 15] + else 15 + ), member=user, created_by_id=project_member_invite.created_by_id, ) @@ -254,9 +255,11 @@ class SignInEndpoint(BaseAPIView): [ ProjectMember( workspace_id=project_member_invite.workspace_id, - role=project_member_invite.role - if project_member_invite.role in [5, 10, 15] - else 15, + role=( + project_member_invite.role + if project_member_invite.role in [5, 10, 15] + else 15 + ), member=user, created_by_id=project_member_invite.created_by_id, ) @@ -392,9 +395,11 @@ class MagicSignInEndpoint(BaseAPIView): [ WorkspaceMember( workspace_id=project_member_invite.workspace_id, - role=project_member_invite.role - if project_member_invite.role in [5, 10, 15] - else 15, + role=( + project_member_invite.role + if project_member_invite.role in [5, 10, 15] + else 15 + ), member=user, created_by_id=project_member_invite.created_by_id, ) @@ -408,9 +413,11 @@ class MagicSignInEndpoint(BaseAPIView): [ ProjectMember( workspace_id=project_member_invite.workspace_id, - role=project_member_invite.role - if project_member_invite.role in [5, 10, 15] - else 15, + role=( + project_member_invite.role + if project_member_invite.role in [5, 10, 15] + else 15 + ), member=user, created_by_id=project_member_invite.created_by_id, ) diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index e691f367a..836fedc9e 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -1,30 +1,30 @@ # Python imports -import zoneinfo import traceback +import zoneinfo + # Django imports -from django.urls import resolve from django.conf import settings -from django.utils import timezone -from django.db import IntegrityError from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.core.serializers.json import DjangoJSONEncoder +from django.db import IntegrityError +from django.urls import resolve +from django.utils import timezone +from django_filters.rest_framework import DjangoFilterBackend # Third part imports from rest_framework import status -from rest_framework import status -from rest_framework.viewsets import ModelViewSet -from rest_framework.response import Response 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.response import Response +from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet from sentry_sdk import capture_exception -from django_filters.rest_framework import DjangoFilterBackend + +from plane.bgtasks.webhook_task import send_webhook # Module imports from plane.utils.paginator import BasePaginator -from plane.bgtasks.webhook_task import send_webhook class TimezoneMixin: @@ -104,7 +104,11 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): response = super().handle_exception(exc) return response except Exception as e: - print(e, traceback.format_exc()) if settings.DEBUG else print("Server Error") + ( + print(e, traceback.format_exc()) + if settings.DEBUG + else print("Server Error") + ) if isinstance(e, IntegrityError): return Response( {"error": "The payload is not valid"}, @@ -119,14 +123,14 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): if isinstance(e, ObjectDoesNotExist): return Response( - {"error": f"The required object does not exist."}, + {"error": "The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) if isinstance(e, KeyError): capture_exception(e) return Response( - {"error": f"The required key does not exist."}, + {"error": "The required key does not exist."}, status=status.HTTP_400_BAD_REQUEST, ) @@ -226,13 +230,13 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): if isinstance(e, ObjectDoesNotExist): return Response( - {"error": f"The required object does not exist."}, + {"error": "The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) if isinstance(e, KeyError): return Response( - {"error": f"The required key does not exist."}, + {"error": "The required key does not exist."}, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/app/views/config.py b/apiserver/plane/app/views/config.py index b2a27252c..066f606b9 100644 --- a/apiserver/plane/app/views/config.py +++ b/apiserver/plane/app/views/config.py @@ -2,7 +2,6 @@ import os # Django imports -from django.conf import settings # Third party imports from rest_framework.permissions import AllowAny @@ -12,13 +11,14 @@ from rest_framework.response import Response # Module imports from .base import BaseAPIView from plane.license.utils.instance_value import get_configuration_value - +from plane.utils.cache import cache_response class ConfigurationEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): # Get all the configuration ( @@ -136,6 +136,7 @@ class MobileConfigurationEndpoint(BaseAPIView): AllowAny, ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): ( GOOGLE_CLIENT_ID, diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle/base.py similarity index 99% rename from apiserver/plane/app/views/cycle.py rename to apiserver/plane/app/views/cycle/base.py index 0c85d4fd5..5204b187b 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1,66 +1,68 @@ # Python imports import json +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.core import serializers + # Django imports from django.db.models import ( - Func, - F, - Q, - Exists, - OuterRef, - Count, - Prefetch, - Sum, Case, - When, - Value, CharField, + Count, + Exists, + F, + Func, + OuterRef, + Prefetch, + Q, + UUIDField, + Value, + When, ) -from django.core import serializers +from django.db.models.functions import Coalesce from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import Value, UUIDField -from django.db.models.functions import Coalesce +from rest_framework import status # Third party imports from rest_framework.response import Response -from rest_framework import status -# Module imports -from . import BaseViewSet, BaseAPIView, WebhookMixin -from plane.app.serializers import ( - CycleSerializer, - CycleIssueSerializer, - CycleFavoriteSerializer, - IssueSerializer, - CycleWriteSerializer, - CycleUserPropertiesSerializer, -) from plane.app.permissions import ( ProjectEntityPermission, ProjectLitePermission, ) -from plane.db.models import ( - User, - Cycle, - CycleIssue, - Issue, - CycleFavorite, - IssueLink, - IssueAttachment, - Label, - CycleUserProperties, +from plane.app.serializers import ( + CycleFavoriteSerializer, + CycleIssueSerializer, + CycleSerializer, + CycleUserPropertiesSerializer, + CycleWriteSerializer, + IssueSerializer, ) from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.issue_filters import issue_filters +from plane.db.models import ( + Cycle, + CycleFavorite, + CycleIssue, + CycleUserProperties, + Issue, + IssueAttachment, + IssueLink, + Label, + User, +) from plane.utils.analytics_plot import burndown_plot -from plane.utils.grouper import issue_queryset_grouper, issue_on_results +from plane.utils.grouper import issue_on_results, issue_queryset_grouper +from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset from plane.utils.paginator import GroupedOffsetPaginator +# Module imports +from .. import BaseAPIView, BaseViewSet, WebhookMixin + + class CycleViewSet(WebhookMixin, BaseViewSet): serializer_class = CycleSerializer model = Cycle @@ -429,9 +431,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet): ) def partial_update(self, request, slug, project_id, pk): - queryset = ( - self.get_queryset() - .filter(workspace__slug=slug, project_id=project_id, pk=pk) + queryset = self.get_queryset().filter( + workspace__slug=slug, project_id=project_id, pk=pk ) cycle = queryset.first() request_data = request.data @@ -790,8 +791,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): ), ) - - def create(self, request, slug, project_id, cycle_id): issues = request.data.get("issues", []) @@ -862,7 +861,9 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): ) # Update the cycle issues - CycleIssue.objects.bulk_update(updated_records, ["cycle_id"], batch_size=100) + CycleIssue.objects.bulk_update( + updated_records, ["cycle_id"], batch_size=100 + ) # Capture Issue Activity issue_activity.delay( type="cycle.activity.created", diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py new file mode 100644 index 000000000..84af4ff32 --- /dev/null +++ b/apiserver/plane/app/views/cycle/issue.py @@ -0,0 +1,312 @@ +# Python imports +import json + +# Django imports +from django.db.models import ( + Func, + F, + Q, + OuterRef, + Value, + UUIDField, +) +from django.core import serializers +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, WebhookMixin +from plane.app.serializers import ( + IssueSerializer, + CycleIssueSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Cycle, + CycleIssue, + Issue, + IssueLink, + IssueAttachment, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class CycleIssueViewSet(WebhookMixin, BaseViewSet): + serializer_class = CycleIssueSerializer + model = CycleIssue + + webhook_event = "cycle_issue" + bulk = True + + permission_classes = [ + ProjectEntityPermission, + ] + + filterset_fields = [ + "issue__labels__id", + "issue__assignees__id", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("issue_id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .filter(cycle_id=self.kwargs.get("cycle_id")) + .select_related("project") + .select_related("workspace") + .select_related("cycle") + .select_related("issue", "issue__state", "issue__project") + .prefetch_related("issue__assignees", "issue__labels") + .distinct() + ) + + @method_decorator(gzip_page) + def list(self, request, slug, project_id, cycle_id): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + order_by = request.GET.get("order_by", "created_at") + filters = issue_filters(request.query_params, "GET") + queryset = ( + Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .filter(**filters) + .select_related("workspace", "project", "state", "parent") + .prefetch_related( + "assignees", + "labels", + "issue_module__module", + "issue_cycle__cycle", + ) + .order_by(order_by) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .order_by(order_by) + ) + if self.fields: + issues = IssueSerializer( + queryset, many=True, fields=fields if fields else None + ).data + else: + issues = queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id, cycle_id): + issues = request.data.get("issues", []) + + if not issues: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=cycle_id + ) + + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): + return Response( + { + "error": "The Cycle has already been completed so no new issues can be added" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get all CycleIssues already created + cycle_issues = list( + CycleIssue.objects.filter( + ~Q(cycle_id=cycle_id), issue_id__in=issues + ) + ) + existing_issues = [ + str(cycle_issue.issue_id) for cycle_issue in cycle_issues + ] + new_issues = list(set(issues) - set(existing_issues)) + + # New issues to create + created_records = CycleIssue.objects.bulk_create( + [ + CycleIssue( + project_id=project_id, + workspace_id=cycle.workspace_id, + created_by_id=request.user.id, + updated_by_id=request.user.id, + cycle_id=cycle_id, + issue_id=issue, + ) + for issue in new_issues + ], + batch_size=10, + ) + + # Updated Issues + updated_records = [] + update_cycle_issue_activity = [] + # Iterate over each cycle_issue in cycle_issues + for cycle_issue in cycle_issues: + # Update the cycle_issue's cycle_id + cycle_issue.cycle_id = cycle_id + # Add the modified cycle_issue to the records_to_update list + updated_records.append(cycle_issue) + # Record the update activity + update_cycle_issue_activity.append( + { + "old_cycle_id": str(cycle_issue.cycle_id), + "new_cycle_id": str(cycle_id), + "issue_id": str(cycle_issue.issue_id), + } + ) + + # Update the cycle issues + CycleIssue.objects.bulk_update( + updated_records, ["cycle_id"], batch_size=100 + ) + # Capture Issue Activity + issue_activity.delay( + type="cycle.activity.created", + requested_data=json.dumps({"cycles_list": issues}), + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "updated_cycle_issues": update_cycle_issue_activity, + "created_cycle_issues": serializers.serialize( + "json", created_records + ), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response({"message": "success"}, status=status.HTTP_201_CREATED) + + def destroy(self, request, slug, project_id, cycle_id, issue_id): + cycle_issue = CycleIssue.objects.get( + issue_id=issue_id, + workspace__slug=slug, + project_id=project_id, + cycle_id=cycle_id, + ) + issue_activity.delay( + type="cycle.activity.deleted", + requested_data=json.dumps( + { + "cycle_id": str(self.kwargs.get("cycle_id")), + "issues": [str(issue_id)], + } + ), + actor_id=str(self.request.user.id), + issue_id=str(issue_id), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + cycle_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/dashboard.py b/apiserver/plane/app/views/dashboard/base.py similarity index 85% rename from apiserver/plane/app/views/dashboard.py rename to apiserver/plane/app/views/dashboard/base.py index 21fe422f9..27e45f59c 100644 --- a/apiserver/plane/app/views/dashboard.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -9,15 +9,15 @@ from django.db.models import ( F, Exists, OuterRef, - Max, Subquery, JSONField, Func, Prefetch, + IntegerField, ) from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField -from django.db.models import Value, UUIDField +from django.db.models import UUIDField from django.db.models.functions import Coalesce from django.utils import timezone @@ -26,7 +26,7 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseAPIView +from .. import BaseAPIView from plane.db.models import ( Issue, IssueActivity, @@ -38,6 +38,8 @@ from plane.db.models import ( IssueLink, IssueAttachment, IssueRelation, + IssueAssignee, + User, ) from plane.app.serializers import ( IssueActivitySerializer, @@ -58,6 +60,7 @@ def dashboard_overview_stats(self, request, slug): pending_issues_count = Issue.issue_objects.filter( ~Q(state__group__in=["completed", "cancelled"]), + target_date__lt=timezone.now().date(), project__project_projectmember__is_active=True, project__project_projectmember__member=request.user, workspace__slug=slug, @@ -211,11 +214,11 @@ def dashboard_assigned_issues(self, request, slug): if issue_type == "overdue": overdue_issues_count = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), ).count() overdue_issues = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), )[:5] return Response( { @@ -230,11 +233,11 @@ def dashboard_assigned_issues(self, request, slug): if issue_type == "upcoming": upcoming_issues_count = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), ).count() upcoming_issues = assigned_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), )[:5] return Response( { @@ -364,11 +367,11 @@ def dashboard_created_issues(self, request, slug): if issue_type == "overdue": overdue_issues_count = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), ).count() overdue_issues = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__lt=timezone.now() + target_date__lt=timezone.now(), )[:5] return Response( { @@ -381,11 +384,11 @@ def dashboard_created_issues(self, request, slug): if issue_type == "upcoming": upcoming_issues_count = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), ).count() upcoming_issues = created_issues.filter( state__group__in=["backlog", "unstarted", "started"], - target_date__gte=timezone.now() + target_date__gte=timezone.now(), )[:5] return Response( { @@ -502,7 +505,9 @@ def dashboard_recent_projects(self, request, slug): ).exclude(id__in=unique_project_ids) # Append additional project IDs to the existing list - unique_project_ids.update(additional_projects.values_list("id", flat=True)) + unique_project_ids.update( + additional_projects.values_list("id", flat=True) + ) return Response( list(unique_project_ids)[:4], @@ -511,90 +516,97 @@ def dashboard_recent_projects(self, request, slug): def dashboard_recent_collaborators(self, request, slug): - # Fetch all project IDs where the user belongs to - user_projects = Project.objects.filter( - project_projectmember__member=request.user, - project_projectmember__is_active=True, - workspace__slug=slug, - ).values_list("id", flat=True) - - # Fetch all users who have performed an activity in the projects where the user exists - users_with_activities = ( + # Subquery to count activities for each project member + activity_count_subquery = ( IssueActivity.objects.filter( workspace__slug=slug, - project_id__in=user_projects, + actor=OuterRef("member"), + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, ) .values("actor") - .exclude(actor=request.user) - .annotate(num_activities=Count("actor")) - .order_by("-num_activities") - )[:7] - - # Get the count of active issues for each user in users_with_activities - users_with_active_issues = [] - for user_activity in users_with_activities: - user_id = user_activity["actor"] - active_issue_count = Issue.objects.filter( - assignees__in=[user_id], - state__group__in=["unstarted", "started"], - ).count() - users_with_active_issues.append( - {"user_id": user_id, "active_issue_count": active_issue_count} - ) - - # Insert the logged-in user's ID and their active issue count at the beginning - active_issue_count = Issue.objects.filter( - assignees__in=[request.user], - state__group__in=["unstarted", "started"], - ).count() - - if users_with_activities.count() < 7: - # Calculate the additional collaborators needed - additional_collaborators_needed = 7 - users_with_activities.count() - - # Fetch additional collaborators from the project_member table - additional_collaborators = list( - set( - ProjectMember.objects.filter( - ~Q(member=request.user), - project_id__in=user_projects, - workspace__slug=slug, - ) - .exclude( - member__in=[ - user["actor"] for user in users_with_activities - ] - ) - .values_list("member", flat=True) - ) - ) - - additional_collaborators = additional_collaborators[ - :additional_collaborators_needed - ] - - # Append additional collaborators to the list - for collaborator_id in additional_collaborators: - active_issue_count = Issue.objects.filter( - assignees__in=[collaborator_id], - state__group__in=["unstarted", "started"], - ).count() - users_with_active_issues.append( - { - "user_id": str(collaborator_id), - "active_issue_count": active_issue_count, - } - ) - - users_with_active_issues.insert( - 0, - {"user_id": request.user.id, "active_issue_count": active_issue_count}, + .annotate(num_activities=Count("pk")) + .values("num_activities") ) - return Response(users_with_active_issues, status=status.HTTP_200_OK) + # Get all project members and annotate them with activity counts + project_members_with_activities = ( + ProjectMember.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .annotate( + num_activities=Coalesce( + Subquery(activity_count_subquery), + Value(0), + output_field=IntegerField(), + ), + is_current_user=Case( + When(member=request.user, then=Value(0)), + default=Value(1), + output_field=IntegerField(), + ), + ) + .values_list("member", flat=True) + .order_by("is_current_user", "-num_activities") + .distinct() + ) + search = request.query_params.get("search", None) + if search: + project_members_with_activities = ( + project_members_with_activities.filter( + Q(member__display_name__icontains=search) + | Q(member__first_name__icontains=search) + | Q(member__last_name__icontains=search) + ) + ) + + return self.paginate( + request=request, + queryset=project_members_with_activities, + controller=self.get_results_controller, + ) class DashboardEndpoint(BaseAPIView): + def get_results_controller(self, project_members_with_activities): + user_active_issue_counts = ( + User.objects.filter(id__in=project_members_with_activities) + .annotate( + active_issue_count=Count( + Case( + When( + issue_assignee__issue__state__group__in=[ + "unstarted", + "started", + ], + then=1, + ), + output_field=IntegerField(), + ) + ) + ) + .values("active_issue_count", user_id=F("id")) + ) + # Create a dictionary to store the active issue counts by user ID + active_issue_counts_dict = { + user["user_id"]: user["active_issue_count"] + for user in user_active_issue_counts + } + + # Preserve the sequence of project members with activities + paginated_results = [ + { + "user_id": member_id, + "active_issue_count": active_issue_counts_dict.get( + member_id, 0 + ), + } + for member_id in project_members_with_activities + ] + return paginated_results + def create(self, request, slug): serializer = DashboardSerializer(data=request.data) if serializer.is_valid(): @@ -621,7 +633,9 @@ class DashboardEndpoint(BaseAPIView): dashboard_type = request.GET.get("dashboard_type", None) if dashboard_type == "home": dashboard, created = Dashboard.objects.get_or_create( - type_identifier=dashboard_type, owned_by=request.user, is_default=True + type_identifier=dashboard_type, + owned_by=request.user, + is_default=True, ) if created: @@ -638,7 +652,9 @@ class DashboardEndpoint(BaseAPIView): updated_dashboard_widgets = [] for widget_key in widgets_to_fetch: - widget = Widget.objects.filter(key=widget_key).values_list("id", flat=True) + widget = Widget.objects.filter( + key=widget_key + ).values_list("id", flat=True) if widget: updated_dashboard_widgets.append( DashboardWidget( diff --git a/apiserver/plane/app/views/error_404.py b/apiserver/plane/app/views/error_404.py new file mode 100644 index 000000000..3c31474e0 --- /dev/null +++ b/apiserver/plane/app/views/error_404.py @@ -0,0 +1,5 @@ +# views.py +from django.http import JsonResponse + +def custom_404_view(request, exception=None): + return JsonResponse({"error": "Page not found."}, status=404) diff --git a/apiserver/plane/app/views/estimate.py b/apiserver/plane/app/views/estimate/base.py similarity index 94% rename from apiserver/plane/app/views/estimate.py rename to apiserver/plane/app/views/estimate/base.py index 3402bb068..7ac3035a9 100644 --- a/apiserver/plane/app/views/estimate.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from .base import BaseViewSet, BaseAPIView +from ..base import BaseViewSet, BaseAPIView from plane.app.permissions import ProjectEntityPermission from plane.db.models import Project, Estimate, EstimatePoint from plane.app.serializers import ( @@ -11,7 +11,7 @@ from plane.app.serializers import ( EstimatePointSerializer, EstimateReadSerializer, ) - +from plane.utils.cache import invalidate_cache class ProjectEstimatePointEndpoint(BaseAPIView): permission_classes = [ @@ -49,6 +49,7 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer = EstimateReadSerializer(estimates, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) def create(self, request, slug, project_id): if not request.data.get("estimate", False): return Response( @@ -114,6 +115,7 @@ class BulkEstimatePointEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) + @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) def partial_update(self, request, slug, project_id, estimate_id): if not request.data.get("estimate", False): return Response( @@ -182,6 +184,7 @@ class BulkEstimatePointEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) + @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) def destroy(self, request, slug, project_id, estimate_id): estimate = Estimate.objects.get( pk=estimate_id, workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/app/views/exporter.py b/apiserver/plane/app/views/exporter/base.py similarity index 95% rename from apiserver/plane/app/views/exporter.py rename to apiserver/plane/app/views/exporter/base.py index 179de81f9..846508515 100644 --- a/apiserver/plane/app/views/exporter.py +++ b/apiserver/plane/app/views/exporter/base.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseAPIView +from .. import BaseAPIView from plane.app.permissions import WorkSpaceAdminPermission from plane.bgtasks.export_task import issue_export_task from plane.db.models import Project, ExporterHistory, Workspace @@ -50,7 +50,7 @@ class ExportIssuesEndpoint(BaseAPIView): ) return Response( { - "message": f"Once the export is ready you will be able to download it" + "message": "Once the export is ready you will be able to download it" }, status=status.HTTP_200_OK, ) diff --git a/apiserver/plane/app/views/external.py b/apiserver/plane/app/views/external/base.py similarity index 91% rename from apiserver/plane/app/views/external.py rename to apiserver/plane/app/views/external/base.py index 618c65e3c..2d5d2c7aa 100644 --- a/apiserver/plane/app/views/external.py +++ b/apiserver/plane/app/views/external/base.py @@ -8,17 +8,15 @@ from rest_framework.response import Response from rest_framework import status # Django imports -from django.conf import settings # Module imports -from .base import BaseAPIView +from ..base import BaseAPIView from plane.app.permissions import ProjectEntityPermission from plane.db.models import Workspace, Project from plane.app.serializers import ( ProjectLiteSerializer, WorkspaceLiteSerializer, ) -from plane.utils.integrations.github import get_release_notes from plane.license.utils.instance_value import get_configuration_value @@ -85,12 +83,6 @@ class GPTIntegrationEndpoint(BaseAPIView): ) -class ReleaseNotesEndpoint(BaseAPIView): - def get(self, request): - release_notes = get_release_notes() - return Response(release_notes, status=status.HTTP_200_OK) - - class UnsplashEndpoint(BaseAPIView): def get(self, request): (UNSPLASH_ACCESS_KEY,) = get_configuration_value( diff --git a/apiserver/plane/app/views/importer.py b/apiserver/plane/app/views/importer.py deleted file mode 100644 index a15ed36b7..000000000 --- a/apiserver/plane/app/views/importer.py +++ /dev/null @@ -1,558 +0,0 @@ -# Python imports -import uuid - -# Third party imports -from rest_framework import status -from rest_framework.response import Response - -# Django imports -from django.db.models import Max, Q - -# Module imports -from plane.app.views import BaseAPIView -from plane.db.models import ( - WorkspaceIntegration, - Importer, - APIToken, - Project, - State, - IssueSequence, - Issue, - IssueActivity, - IssueComment, - IssueLink, - IssueLabel, - Workspace, - IssueAssignee, - Module, - ModuleLink, - ModuleIssue, - Label, -) -from plane.app.serializers import ( - ImporterSerializer, - IssueFlatSerializer, - ModuleSerializer, -) -from plane.utils.integrations.github import get_github_repo_details -from plane.utils.importers.jira import ( - jira_project_issue_summary, - is_allowed_hostname, -) -from plane.bgtasks.importer_task import service_importer -from plane.utils.html_processor import strip_tags -from plane.app.permissions import WorkSpaceAdminPermission - - -class ServiceIssueImportSummaryEndpoint(BaseAPIView): - def get(self, request, slug, service): - if service == "github": - owner = request.GET.get("owner", False) - repo = request.GET.get("repo", False) - - if not owner or not repo: - return Response( - {"error": "Owner and repo are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace_integration = WorkspaceIntegration.objects.get( - integration__provider="github", workspace__slug=slug - ) - - access_tokens_url = workspace_integration.metadata.get( - "access_tokens_url", False - ) - - if not access_tokens_url: - return Response( - { - "error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - issue_count, labels, collaborators = get_github_repo_details( - access_tokens_url, owner, repo - ) - return Response( - { - "issue_count": issue_count, - "labels": labels, - "collaborators": collaborators, - }, - status=status.HTTP_200_OK, - ) - - if service == "jira": - # Check for all the keys - params = { - "project_key": "Project key is required", - "api_token": "API token is required", - "email": "Email is required", - "cloud_hostname": "Cloud hostname is required", - } - - for key, error_message in params.items(): - if not request.GET.get(key, False): - return Response( - {"error": error_message}, - status=status.HTTP_400_BAD_REQUEST, - ) - - project_key = request.GET.get("project_key", "") - api_token = request.GET.get("api_token", "") - email = request.GET.get("email", "") - cloud_hostname = request.GET.get("cloud_hostname", "") - - response = jira_project_issue_summary( - email, api_token, project_key, cloud_hostname - ) - if "error" in response: - return Response(response, status=status.HTTP_400_BAD_REQUEST) - else: - return Response( - response, - status=status.HTTP_200_OK, - ) - return Response( - {"error": "Service not supported yet"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class ImportServiceEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] - - def post(self, request, slug, service): - project_id = request.data.get("project_id", False) - - if not project_id: - return Response( - {"error": "Project ID is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - if service == "github": - data = request.data.get("data", False) - metadata = request.data.get("metadata", False) - config = request.data.get("config", False) - if not data or not metadata or not config: - return Response( - {"error": "Data, config and metadata are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - api_token = APIToken.objects.filter( - user=request.user, workspace=workspace - ).first() - if api_token is None: - api_token = APIToken.objects.create( - user=request.user, - label="Importer", - workspace=workspace, - ) - - importer = Importer.objects.create( - service=service, - project_id=project_id, - status="queued", - initiated_by=request.user, - data=data, - metadata=metadata, - token=api_token, - config=config, - created_by=request.user, - updated_by=request.user, - ) - - service_importer.delay(service, importer.id) - serializer = ImporterSerializer(importer) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - if service == "jira": - data = request.data.get("data", False) - metadata = request.data.get("metadata", False) - config = request.data.get("config", False) - - cloud_hostname = metadata.get("cloud_hostname", False) - - if not cloud_hostname: - return Response( - {"error": "Cloud hostname is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not is_allowed_hostname(cloud_hostname): - return Response( - {"error": "Hostname is not a valid hostname."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not data or not metadata: - return Response( - {"error": "Data, config and metadata are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - api_token = APIToken.objects.filter( - user=request.user, workspace=workspace - ).first() - if api_token is None: - api_token = APIToken.objects.create( - user=request.user, - label="Importer", - workspace=workspace, - ) - - importer = Importer.objects.create( - service=service, - project_id=project_id, - status="queued", - initiated_by=request.user, - data=data, - metadata=metadata, - token=api_token, - config=config, - created_by=request.user, - updated_by=request.user, - ) - - service_importer.delay(service, importer.id) - serializer = ImporterSerializer(importer) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response( - {"error": "Servivce not supported yet"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get(self, request, slug): - imports = ( - Importer.objects.filter(workspace__slug=slug) - .order_by("-created_at") - .select_related("initiated_by", "project", "workspace") - ) - serializer = ImporterSerializer(imports, many=True) - return Response(serializer.data) - - def delete(self, request, slug, service, pk): - importer = Importer.objects.get( - pk=pk, service=service, workspace__slug=slug - ) - - if importer.imported_data is not None: - # Delete all imported Issues - imported_issues = importer.imported_data.get("issues", []) - Issue.issue_objects.filter(id__in=imported_issues).delete() - - # Delete all imported Labels - imported_labels = importer.imported_data.get("labels", []) - Label.objects.filter(id__in=imported_labels).delete() - - if importer.service == "jira": - imported_modules = importer.imported_data.get("modules", []) - Module.objects.filter(id__in=imported_modules).delete() - importer.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - def patch(self, request, slug, service, pk): - importer = Importer.objects.get( - pk=pk, service=service, workspace__slug=slug - ) - serializer = ImporterSerializer( - importer, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class UpdateServiceImportStatusEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service, importer_id): - importer = Importer.objects.get( - pk=importer_id, - workspace__slug=slug, - project_id=project_id, - service=service, - ) - importer.status = request.data.get("status", "processing") - importer.save() - return Response(status.HTTP_200_OK) - - -class BulkImportIssuesEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service): - # Get the project - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - # Get the default state - default_state = State.objects.filter( - ~Q(name="Triage"), project_id=project_id, default=True - ).first() - # if there is no default state assign any random state - if default_state is None: - default_state = State.objects.filter( - ~Q(name="Triage"), project_id=project_id - ).first() - - # Get the maximum sequence_id - last_id = IssueSequence.objects.filter( - project_id=project_id - ).aggregate(largest=Max("sequence"))["largest"] - - last_id = 1 if last_id is None else last_id + 1 - - # Get the maximum sort order - largest_sort_order = Issue.objects.filter( - project_id=project_id, state=default_state - ).aggregate(largest=Max("sort_order"))["largest"] - - largest_sort_order = ( - 65535 if largest_sort_order is None else largest_sort_order + 10000 - ) - - # Get the issues_data - issues_data = request.data.get("issues_data", []) - - if not len(issues_data): - return Response( - {"error": "Issue data is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Issues - bulk_issues = [] - for issue_data in issues_data: - bulk_issues.append( - Issue( - project_id=project_id, - workspace_id=project.workspace_id, - state_id=issue_data.get("state") - if issue_data.get("state", False) - else default_state.id, - name=issue_data.get("name", "Issue Created through Bulk"), - description_html=issue_data.get( - "description_html", "

" - ), - description_stripped=( - None - if ( - issue_data.get("description_html") == "" - or issue_data.get("description_html") is None - ) - else strip_tags(issue_data.get("description_html")) - ), - sequence_id=last_id, - sort_order=largest_sort_order, - start_date=issue_data.get("start_date", None), - target_date=issue_data.get("target_date", None), - priority=issue_data.get("priority", "none"), - created_by=request.user, - ) - ) - - largest_sort_order = largest_sort_order + 10000 - last_id = last_id + 1 - - issues = Issue.objects.bulk_create( - bulk_issues, - batch_size=100, - ignore_conflicts=True, - ) - - # Sequences - _ = IssueSequence.objects.bulk_create( - [ - IssueSequence( - issue=issue, - sequence=issue.sequence_id, - project_id=project_id, - workspace_id=project.workspace_id, - ) - for issue in issues - ], - batch_size=100, - ) - - # Attach Labels - bulk_issue_labels = [] - for issue, issue_data in zip(issues, issues_data): - labels_list = issue_data.get("labels_list", []) - bulk_issue_labels = bulk_issue_labels + [ - IssueLabel( - issue=issue, - label_id=label_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for label_id in labels_list - ] - - _ = IssueLabel.objects.bulk_create( - bulk_issue_labels, batch_size=100, ignore_conflicts=True - ) - - # Attach Assignees - bulk_issue_assignees = [] - for issue, issue_data in zip(issues, issues_data): - assignees_list = issue_data.get("assignees_list", []) - bulk_issue_assignees = bulk_issue_assignees + [ - IssueAssignee( - issue=issue, - assignee_id=assignee_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for assignee_id in assignees_list - ] - - _ = IssueAssignee.objects.bulk_create( - bulk_issue_assignees, batch_size=100, ignore_conflicts=True - ) - - # Track the issue activities - IssueActivity.objects.bulk_create( - [ - IssueActivity( - issue=issue, - actor=request.user, - project_id=project_id, - workspace_id=project.workspace_id, - comment=f"imported the issue from {service}", - verb="created", - created_by=request.user, - ) - for issue in issues - ], - batch_size=100, - ) - - # Create Comments - bulk_issue_comments = [] - for issue, issue_data in zip(issues, issues_data): - comments_list = issue_data.get("comments_list", []) - bulk_issue_comments = bulk_issue_comments + [ - IssueComment( - issue=issue, - comment_html=comment.get("comment_html", "

"), - actor=request.user, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for comment in comments_list - ] - - _ = IssueComment.objects.bulk_create( - bulk_issue_comments, batch_size=100 - ) - - # Attach Links - _ = IssueLink.objects.bulk_create( - [ - IssueLink( - issue=issue, - url=issue_data.get("link", {}).get( - "url", "https://github.com" - ), - title=issue_data.get("link", {}).get( - "title", "Original Issue" - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for issue, issue_data in zip(issues, issues_data) - ] - ) - - return Response( - {"issues": IssueFlatSerializer(issues, many=True).data}, - status=status.HTTP_201_CREATED, - ) - - -class BulkImportModulesEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service): - modules_data = request.data.get("modules_data", []) - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - modules = Module.objects.bulk_create( - [ - Module( - name=module.get("name", uuid.uuid4().hex), - description=module.get("description", ""), - start_date=module.get("start_date", None), - target_date=module.get("target_date", None), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for module in modules_data - ], - batch_size=100, - ignore_conflicts=True, - ) - - modules = Module.objects.filter( - id__in=[module.id for module in modules] - ) - - if len(modules) == len(modules_data): - _ = ModuleLink.objects.bulk_create( - [ - ModuleLink( - module=module, - url=module_data.get("link", {}).get( - "url", "https://plane.so" - ), - title=module_data.get("link", {}).get( - "title", "Original Issue" - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for module, module_data in zip(modules, modules_data) - ], - batch_size=100, - ignore_conflicts=True, - ) - - bulk_module_issues = [] - for module, module_data in zip(modules, modules_data): - module_issues_list = module_data.get("module_issues_list", []) - bulk_module_issues = bulk_module_issues + [ - ModuleIssue( - issue_id=issue, - module=module, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for issue in module_issues_list - ] - - _ = ModuleIssue.objects.bulk_create( - bulk_module_issues, batch_size=100, ignore_conflicts=True - ) - - serializer = ModuleSerializer(modules, many=True) - return Response( - {"modules": serializer.data}, status=status.HTTP_201_CREATED - ) - - else: - return Response( - { - "message": "Modules created but issues could not be imported" - }, - status=status.HTTP_200_OK, - ) diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox/base.py similarity index 98% rename from apiserver/plane/app/views/inbox.py rename to apiserver/plane/app/views/inbox/base.py index ed32a14fe..fb3b9227f 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -15,7 +15,7 @@ from rest_framework import status from rest_framework.response import Response # Module imports -from .base import BaseViewSet +from ..base import BaseViewSet from plane.app.permissions import ProjectBasePermission, ProjectLitePermission from plane.db.models import ( Inbox, @@ -213,7 +213,7 @@ class InboxIssueViewSet(BaseViewSet): ) # Check for valid priority - if not request.data.get("issue", {}).get("priority", "none") in [ + if request.data.get("issue", {}).get("priority", "none") not in [ "low", "medium", "high", @@ -428,8 +428,11 @@ class InboxIssueViewSet(BaseViewSet): ) ).first() if issue is None: - return Response({"error": "Requested object was not found"}, status=status.HTTP_404_NOT_FOUND) - + return Response( + {"error": "Requested object was not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/integration/__init__.py b/apiserver/plane/app/views/integration/__init__.py deleted file mode 100644 index ea20d96ea..000000000 --- a/apiserver/plane/app/views/integration/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .base import IntegrationViewSet, WorkspaceIntegrationViewSet -from .github import ( - GithubRepositorySyncViewSet, - GithubIssueSyncViewSet, - BulkCreateGithubIssueSyncEndpoint, - GithubCommentSyncViewSet, - GithubRepositoriesEndpoint, -) -from .slack import SlackProjectSyncViewSet diff --git a/apiserver/plane/app/views/integration/base.py b/apiserver/plane/app/views/integration/base.py deleted file mode 100644 index d757fe471..000000000 --- a/apiserver/plane/app/views/integration/base.py +++ /dev/null @@ -1,181 +0,0 @@ -# Python improts -import uuid -import requests - -# Django imports -from django.contrib.auth.hashers import make_password - -# Third party imports -from rest_framework.response import Response -from rest_framework import status -from sentry_sdk import capture_exception - -# Module imports -from plane.app.views import BaseViewSet -from plane.db.models import ( - Integration, - WorkspaceIntegration, - Workspace, - User, - WorkspaceMember, - APIToken, -) -from plane.app.serializers import ( - IntegrationSerializer, - WorkspaceIntegrationSerializer, -) -from plane.utils.integrations.github import ( - get_github_metadata, - delete_github_installation, -) -from plane.app.permissions import WorkSpaceAdminPermission -from plane.utils.integrations.slack import slack_oauth - - -class IntegrationViewSet(BaseViewSet): - serializer_class = IntegrationSerializer - model = Integration - - def create(self, request): - serializer = IntegrationSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, pk): - integration = Integration.objects.get(pk=pk) - if integration.verified: - return Response( - {"error": "Verified integrations cannot be updated"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = IntegrationSerializer( - integration, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, pk): - integration = Integration.objects.get(pk=pk) - if integration.verified: - return Response( - {"error": "Verified integrations cannot be updated"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - integration.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkspaceIntegrationViewSet(BaseViewSet): - serializer_class = WorkspaceIntegrationSerializer - model = WorkspaceIntegration - - permission_classes = [ - WorkSpaceAdminPermission, - ] - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("integration") - ) - - def create(self, request, slug, provider): - workspace = Workspace.objects.get(slug=slug) - integration = Integration.objects.get(provider=provider) - config = {} - if provider == "github": - installation_id = request.data.get("installation_id", None) - if not installation_id: - return Response( - {"error": "Installation ID is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - metadata = get_github_metadata(installation_id) - config = {"installation_id": installation_id} - - if provider == "slack": - code = request.data.get("code", False) - - if not code: - return Response( - {"error": "Code is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - slack_response = slack_oauth(code=code) - - metadata = slack_response - access_token = metadata.get("access_token", False) - team_id = metadata.get("team", {}).get("id", False) - if not metadata or not access_token or not team_id: - return Response( - { - "error": "Slack could not be installed. Please try again later" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - config = {"team_id": team_id, "access_token": access_token} - - # Create a bot user - bot_user = User.objects.create( - email=f"{uuid.uuid4().hex}@plane.so", - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, - is_bot=True, - first_name=integration.title, - avatar=integration.avatar_url - if integration.avatar_url is not None - else "", - ) - - # Create an API Token for the bot user - api_token = APIToken.objects.create( - user=bot_user, - user_type=1, # bot user - workspace=workspace, - ) - - workspace_integration = WorkspaceIntegration.objects.create( - workspace=workspace, - integration=integration, - actor=bot_user, - api_token=api_token, - metadata=metadata, - config=config, - ) - - # Add bot user as a member of workspace - _ = WorkspaceMember.objects.create( - workspace=workspace_integration.workspace, - member=bot_user, - role=20, - ) - return Response( - WorkspaceIntegrationSerializer(workspace_integration).data, - status=status.HTTP_201_CREATED, - ) - - def destroy(self, request, slug, pk): - workspace_integration = WorkspaceIntegration.objects.get( - pk=pk, workspace__slug=slug - ) - - if workspace_integration.integration.provider == "github": - installation_id = workspace_integration.config.get( - "installation_id", False - ) - if installation_id: - delete_github_installation(installation_id=installation_id) - - workspace_integration.delete() - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/integration/github.py b/apiserver/plane/app/views/integration/github.py deleted file mode 100644 index 2d37c64b0..000000000 --- a/apiserver/plane/app/views/integration/github.py +++ /dev/null @@ -1,202 +0,0 @@ -# Third party imports -from rest_framework import status -from rest_framework.response import Response -from sentry_sdk import capture_exception - -# Module imports -from plane.app.views import BaseViewSet, BaseAPIView -from plane.db.models import ( - GithubIssueSync, - GithubRepositorySync, - GithubRepository, - WorkspaceIntegration, - ProjectMember, - Label, - GithubCommentSync, - Project, -) -from plane.app.serializers import ( - GithubIssueSyncSerializer, - GithubRepositorySyncSerializer, - GithubCommentSyncSerializer, -) -from plane.utils.integrations.github import get_github_repos -from plane.app.permissions import ( - ProjectBasePermission, - ProjectEntityPermission, -) - - -class GithubRepositoriesEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - - def get(self, request, slug, workspace_integration_id): - page = request.GET.get("page", 1) - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) - - if workspace_integration.integration.provider != "github": - return Response( - {"error": "Not a github integration"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - access_tokens_url = workspace_integration.metadata["access_tokens_url"] - repositories_url = ( - workspace_integration.metadata["repositories_url"] - + f"?per_page=100&page={page}" - ) - repositories = get_github_repos(access_tokens_url, repositories_url) - return Response(repositories, status=status.HTTP_200_OK) - - -class GithubRepositorySyncViewSet(BaseViewSet): - permission_classes = [ - ProjectBasePermission, - ] - - serializer_class = GithubRepositorySyncSerializer - model = GithubRepositorySync - - def perform_create(self, serializer): - serializer.save(project_id=self.kwargs.get("project_id")) - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - ) - - def create(self, request, slug, project_id, workspace_integration_id): - name = request.data.get("name", False) - url = request.data.get("url", False) - config = request.data.get("config", {}) - repository_id = request.data.get("repository_id", False) - owner = request.data.get("owner", False) - - if not name or not url or not repository_id or not owner: - return Response( - {"error": "Name, url, repository_id and owner are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the workspace integration - workspace_integration = WorkspaceIntegration.objects.get( - pk=workspace_integration_id - ) - - # Delete the old repository object - GithubRepositorySync.objects.filter( - project_id=project_id, workspace__slug=slug - ).delete() - GithubRepository.objects.filter( - project_id=project_id, workspace__slug=slug - ).delete() - - # Create repository - repo = GithubRepository.objects.create( - name=name, - url=url, - config=config, - repository_id=repository_id, - owner=owner, - project_id=project_id, - ) - - # Create a Label for github - label = Label.objects.filter( - name="GitHub", - project_id=project_id, - ).first() - - if label is None: - label = Label.objects.create( - name="GitHub", - project_id=project_id, - description="Label to sync Plane issues with GitHub issues", - color="#003773", - ) - - # Create repo sync - repo_sync = GithubRepositorySync.objects.create( - repository=repo, - workspace_integration=workspace_integration, - actor=workspace_integration.actor, - credentials=request.data.get("credentials", {}), - project_id=project_id, - label=label, - ) - - # Add bot as a member in the project - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, role=20, project_id=project_id - ) - - # Return Response - return Response( - GithubRepositorySyncSerializer(repo_sync).data, - status=status.HTTP_201_CREATED, - ) - - -class GithubIssueSyncViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - - serializer_class = GithubIssueSyncSerializer - model = GithubIssueSync - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - repository_sync_id=self.kwargs.get("repo_sync_id"), - ) - - -class BulkCreateGithubIssueSyncEndpoint(BaseAPIView): - def post(self, request, slug, project_id, repo_sync_id): - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - github_issue_syncs = request.data.get("github_issue_syncs", []) - github_issue_syncs = GithubIssueSync.objects.bulk_create( - [ - GithubIssueSync( - issue_id=github_issue_sync.get("issue"), - repo_issue_id=github_issue_sync.get("repo_issue_id"), - issue_url=github_issue_sync.get("issue_url"), - github_issue_id=github_issue_sync.get("github_issue_id"), - repository_sync_id=repo_sync_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for github_issue_sync in github_issue_syncs - ], - batch_size=100, - ignore_conflicts=True, - ) - - serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - -class GithubCommentSyncViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - - serializer_class = GithubCommentSyncSerializer - model = GithubCommentSync - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - issue_sync_id=self.kwargs.get("issue_sync_id"), - ) diff --git a/apiserver/plane/app/views/integration/slack.py b/apiserver/plane/app/views/integration/slack.py deleted file mode 100644 index c22ee3e52..000000000 --- a/apiserver/plane/app/views/integration/slack.py +++ /dev/null @@ -1,96 +0,0 @@ -# Django import -from django.db import IntegrityError - -# Third party imports -from rest_framework import status -from rest_framework.response import Response -from sentry_sdk import capture_exception - -# Module imports -from plane.app.views import BaseViewSet, BaseAPIView -from plane.db.models import ( - SlackProjectSync, - WorkspaceIntegration, - ProjectMember, -) -from plane.app.serializers import SlackProjectSyncSerializer -from plane.app.permissions import ( - ProjectBasePermission, - ProjectEntityPermission, -) -from plane.utils.integrations.slack import slack_oauth - - -class SlackProjectSyncViewSet(BaseViewSet): - permission_classes = [ - ProjectBasePermission, - ] - serializer_class = SlackProjectSyncSerializer - model = SlackProjectSync - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - ) - - def create(self, request, slug, project_id, workspace_integration_id): - try: - code = request.data.get("code", False) - - if not code: - return Response( - {"error": "Code is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - slack_response = slack_oauth(code=code) - - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) - - workspace_integration = WorkspaceIntegration.objects.get( - pk=workspace_integration_id, workspace__slug=slug - ) - slack_project_sync = SlackProjectSync.objects.create( - access_token=slack_response.get("access_token"), - scopes=slack_response.get("scope"), - bot_user_id=slack_response.get("bot_user_id"), - webhook_url=slack_response.get("incoming_webhook", {}).get( - "url" - ), - data=slack_response, - team_id=slack_response.get("team", {}).get("id"), - team_name=slack_response.get("team", {}).get("name"), - workspace_integration=workspace_integration, - project_id=project_id, - ) - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, - role=20, - project_id=project_id, - ) - serializer = SlackProjectSyncSerializer(slack_project_sync) - return Response(serializer.data, status=status.HTTP_200_OK) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "Slack is already installed for the project"}, - status=status.HTTP_410_GONE, - ) - capture_exception(e) - return Response( - { - "error": "Slack could not be installed. Please try again later" - }, - status=status.HTTP_400_BAD_REQUEST, - ) diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py deleted file mode 100644 index f5e020a08..000000000 --- a/apiserver/plane/app/views/issue.py +++ /dev/null @@ -1,2036 +0,0 @@ -# Python imports -import json -import random -from itertools import chain -from collections import defaultdict - -# Django imports -from django.utils import timezone -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Exists, -) -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 import IntegrityError -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import Value, UUIDField -from django.db.models.functions import Coalesce - -# Third Party imports -from rest_framework.response import Response -from rest_framework import status -from rest_framework.parsers import MultiPartParser, FormParser - -# Module imports -from . import BaseViewSet, BaseAPIView, WebhookMixin -from plane.app.serializers import ( - IssueActivitySerializer, - IssueCommentSerializer, - IssuePropertySerializer, - IssueSerializer, - IssueCreateSerializer, - LabelSerializer, - IssueFlatSerializer, - IssueLinkSerializer, - IssueLiteSerializer, - IssueAttachmentSerializer, - IssueSubscriberSerializer, - ProjectMemberLiteSerializer, - IssueReactionSerializer, - CommentReactionSerializer, - IssueRelationSerializer, - RelatedIssueSerializer, - IssueDetailSerializer, -) -from plane.app.permissions import ( - ProjectEntityPermission, - WorkSpaceAdminPermission, - ProjectMemberPermission, - ProjectLitePermission, -) -from plane.db.models import ( - Project, - Issue, - State, - IssueActivity, - IssueComment, - IssueProperty, - Label, - IssueLink, - IssueAttachment, - IssueSubscriber, - ProjectMember, - IssueReaction, - CommentReaction, - IssueRelation, -) -from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.grouper import ( - issue_queryset_grouper, - issue_on_results, - issue_group_values, -) -from plane.utils.issue_filters import issue_filters -from plane.utils.order_queryset import order_issue_queryset -from plane.utils.paginator import GroupedOffsetPaginator - - -class IssueListEndpoint(BaseAPIView): - - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - issue_ids = request.GET.get("issues", False) - - if not issue_ids: - return Response( - {"error": "Issues are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - issue_ids = [ - issue_id for issue_id in issue_ids.split(",") if issue_id != "" - ] - - queryset = ( - Issue.issue_objects.filter( - workspace__slug=slug, project_id=project_id, pk__in=issue_ids - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - ).distinct() - - filters = issue_filters(request.query_params, "GET") - - order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = queryset.filter(**filters) - # Issue queryset - issue_queryset = order_issue_queryset( - issue_queryset=issue_queryset, - order_by_param=order_by_param, - ) - - # Group by - group_by = request.GET.get("group_by", False) - issue_queryset = issue_queryset_grouper( - queryset=issue_queryset, field=group_by - ) - - # List Paginate - if not group_by: - return self.paginate( - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues - ), - ) - - # Group paginate - return self.paginate( - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues - ), - paginator_cls=GroupedOffsetPaginator, - group_by_field_name=group_by, - count_filter=Q( - Q(issue_inbox__status=1) - | Q(issue_inbox__status=-1) - | Q(issue_inbox__status=2) - | Q(issue_inbox__isnull=True), - archived_at__isnull=False, - is_draft=True, - ), - ) - - -class IssueViewSet(WebhookMixin, BaseViewSet): - def get_serializer_class(self): - return ( - IssueCreateSerializer - if self.action in ["create", "update", "partial_update"] - else IssueSerializer - ) - - model = Issue - webhook_event = "issue" - permission_classes = [ - ProjectEntityPermission, - ] - - search_fields = [ - "name", - ] - - filterset_fields = [ - "state__name", - "assignees__id", - "workspace__id", - ] - - def get_queryset(self): - return ( - Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id") - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - # Custom ordering for priority and state - - # Issue queryset - issue_queryset = order_issue_queryset( - issue_queryset=issue_queryset, - order_by_param=order_by_param, - ) - - # Group by - group_by = request.GET.get("group_by", False) - - # issue queryset - issue_queryset = issue_queryset_grouper( - queryset=issue_queryset, field=group_by - ) - - # List Paginate - if not group_by: - return self.paginate( - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues - ), - ) - - # Group paginate - return self.paginate( - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues - ), - paginator_cls=GroupedOffsetPaginator, - group_by_fields=issue_group_values( - field=group_by, - slug=slug, - project_id=project_id, - ), - group_by_field_name=group_by, - count_filter=Q( - Q(issue_inbox__status=1) - | Q(issue_inbox__status=-1) - | Q(issue_inbox__status=2) - | Q(issue_inbox__isnull=True), - archived_at__isnull=False, - is_draft=True, - ), - ) - - def create(self, request, slug, project_id): - project = Project.objects.get(pk=project_id) - - serializer = IssueCreateSerializer( - data=request.data, - context={ - "project_id": project_id, - "workspace_id": project.workspace_id, - "default_assignee_id": project.default_assignee_id, - }, - ) - - if serializer.is_valid(): - serializer.save() - - # Track the issue - issue_activity.delay( - type="issue.activity.created", - requested_data=json.dumps( - self.request.data, cls=DjangoJSONEncoder - ), - actor_id=str(request.user.id), - issue_id=str(serializer.data.get("id", None)), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue = ( - self.get_queryset() - .filter(pk=serializer.data["id"]) - .values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - .first() - ) - return Response(issue, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def partial_update(self, request, slug, project_id, pk=None): - issue = self.get_queryset().filter(pk=pk).first() - - if not issue: - return Response( - {"error": "Issue not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - current_instance = json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ) - - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - serializer = IssueCreateSerializer( - issue, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="issue.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue = self.get_queryset().filter(pk=pk).first() - return Response(status=status.HTTP_204_NO_CONTENT) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - issue.delete() - issue_activity.delay( - type="issue.activity.deleted", - requested_data=json.dumps({"issue_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance={}, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - - -# TODO: deprecated remove once confirmed -class UserWorkSpaceIssues(BaseAPIView): - @method_decorator(gzip_page) - def get(self, request, slug): - filters = issue_filters(request.query_params, "GET") - # Custom ordering for priority and state - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - Issue.issue_objects.filter( - ( - Q(assignees__in=[request.user]) - | Q(created_by=request.user) - | Q(issue_subscribers__subscriber=request.user) - ), - workspace__slug=slug, - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .select_related("project") - .select_related("workspace") - .select_related("state") - .select_related("parent") - .prefetch_related("assignees") - .prefetch_related("labels") - .order_by(order_by_param) - .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") - ) - .filter(**filters) - ).distinct() - - issue_queryset = order_issue_queryset( - issue_queryset=issue_queryset, order_by_param=order_by_param - ) - - issues = IssueLiteSerializer(issue_queryset, many=True).data - - ## Grouping the results - group_by = request.GET.get("group_by", False) - sub_group_by = request.GET.get("sub_group_by", False) - if sub_group_by and sub_group_by == group_by: - return Response( - {"error": "Group by and sub group by cannot be same"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # if group_by: - # grouped_results = group_results(issues, group_by, sub_group_by) - # return Response( - # grouped_results, - # status=status.HTTP_200_OK, - # ) - - return Response(issues, status=status.HTTP_200_OK) - - -# TODO: deprecated remove once confirmed -class WorkSpaceIssuesEndpoint(BaseAPIView): - permission_classes = [ - WorkSpaceAdminPermission, - ] - - @method_decorator(gzip_page) - def get(self, request, slug): - issues = ( - Issue.issue_objects.filter(workspace__slug=slug) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - ) - serializer = IssueSerializer(issues, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class IssueActivityEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - @method_decorator(gzip_page) - def get(self, request, slug, project_id, issue_id): - filters = {} - if request.GET.get("created_at__gt", None) is not None: - filters = {"created_at__gt": request.GET.get("created_at__gt")} - - issue_activities = ( - IssueActivity.objects.filter(issue_id=issue_id) - .filter( - ~Q(field__in=["comment", "vote", "reaction", "draft"]), - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - workspace__slug=slug, - ) - .filter(**filters) - .select_related("actor", "workspace", "issue", "project") - ).order_by("created_at") - issue_comments = ( - IssueComment.objects.filter(issue_id=issue_id) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - workspace__slug=slug, - ) - .filter(**filters) - .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 - - if request.GET.get("activity_type", None) == "issue-property": - return Response(issue_activities, status=status.HTTP_200_OK) - - if request.GET.get("activity_type", None) == "issue-comment": - return Response(issue_comments, status=status.HTTP_200_OK) - - result_list = sorted( - chain(issue_activities, issue_comments), - key=lambda instance: instance["created_at"], - ) - - return Response(result_list, status=status.HTTP_200_OK) - - -class IssueCommentViewSet(WebhookMixin, BaseViewSet): - serializer_class = IssueCommentSerializer - model = IssueComment - webhook_event = "issue_comment" - permission_classes = [ - ProjectLitePermission, - ] - - filterset_fields = [ - "issue__id", - "workspace__id", - ] - - def get_queryset(self): - return self.filter_queryset( - 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")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .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, - is_active=True, - ) - ) - ) - .distinct() - ) - - def create(self, request, slug, project_id, issue_id): - serializer = IssueCommentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - issue_id=issue_id, - actor=request.user, - ) - issue_activity.delay( - type="comment.activity.created", - requested_data=json.dumps( - serializer.data, cls=DjangoJSONEncoder - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id")), - project_id=str(self.kwargs.get("project_id")), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, issue_id, pk): - issue_comment = IssueComment.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - current_instance = json.dumps( - IssueCommentSerializer(issue_comment).data, - cls=DjangoJSONEncoder, - ) - serializer = IssueCommentSerializer( - issue_comment, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="comment.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, pk): - issue_comment = IssueComment.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - current_instance = json.dumps( - IssueCommentSerializer(issue_comment).data, - cls=DjangoJSONEncoder, - ) - issue_comment.delete() - issue_activity.delay( - type="comment.activity.deleted", - requested_data=json.dumps({"comment_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueUserDisplayPropertyEndpoint(BaseAPIView): - permission_classes = [ - ProjectLitePermission, - ] - - def patch(self, request, slug, project_id): - issue_property = IssueProperty.objects.get( - user=request.user, - project_id=project_id, - ) - - issue_property.filters = request.data.get( - "filters", issue_property.filters - ) - issue_property.display_filters = request.data.get( - "display_filters", issue_property.display_filters - ) - issue_property.display_properties = request.data.get( - "display_properties", issue_property.display_properties - ) - issue_property.save() - serializer = IssuePropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def get(self, request, slug, project_id): - issue_property, _ = IssueProperty.objects.get_or_create( - user=request.user, project_id=project_id - ) - serializer = IssuePropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class LabelViewSet(BaseViewSet): - serializer_class = LabelSerializer - model = Label - permission_classes = [ - ProjectMemberPermission, - ] - - def create(self, request, slug, project_id): - try: - serializer = LabelSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(project_id=project_id) - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - except IntegrityError: - return Response( - { - "error": "Label with the same name already exists in the project" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .select_related("project") - .select_related("workspace") - .select_related("parent") - .distinct() - .order_by("sort_order") - ) - - -class BulkDeleteIssuesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def delete(self, request, slug, project_id): - issue_ids = request.data.get("issue_ids", []) - - if not len(issue_ids): - return Response( - {"error": "Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - issues = Issue.issue_objects.filter( - workspace__slug=slug, project_id=project_id, pk__in=issue_ids - ) - - total_issues = len(issues) - - issues.delete() - - return Response( - {"message": f"{total_issues} issues were deleted"}, - status=status.HTTP_200_OK, - ) - - -class SubIssuesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - @method_decorator(gzip_page) - def get(self, request, slug, project_id, issue_id): - sub_issues = ( - Issue.issue_objects.filter( - parent_id=issue_id, workspace__slug=slug - ) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .annotate(state_group=F("state__group")) - ) - - # create's a dict with state group name with their respective issue id's - result = defaultdict(list) - for sub_issue in sub_issues: - result[sub_issue.state_group].append(str(sub_issue.id)) - - sub_issues = sub_issues.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response( - { - "sub_issues": sub_issues, - "state_distribution": result, - }, - status=status.HTTP_200_OK, - ) - - # Assign multiple sub issues - def post(self, request, slug, project_id, issue_id): - parent_issue = Issue.issue_objects.get(pk=issue_id) - sub_issue_ids = request.data.get("sub_issue_ids", []) - - if not len(sub_issue_ids): - return Response( - {"error": "Sub Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) - - for sub_issue in sub_issues: - sub_issue.parent = parent_issue - - _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) - - updated_sub_issues = Issue.issue_objects.filter( - id__in=sub_issue_ids - ).annotate(state_group=F("state__group")) - - # Track the issue - _ = [ - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"parent": str(issue_id)}), - actor_id=str(request.user.id), - issue_id=str(sub_issue_id), - project_id=str(project_id), - current_instance=json.dumps({"parent": str(sub_issue_id)}), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - for sub_issue_id in sub_issue_ids - ] - - # create's a dict with state group name with their respective issue id's - result = defaultdict(list) - for sub_issue in updated_sub_issues: - result[sub_issue.state_group].append(str(sub_issue.id)) - - serializer = IssueSerializer( - updated_sub_issues, - many=True, - ) - return Response( - { - "sub_issues": serializer.data, - "state_distribution": result, - }, - status=status.HTTP_200_OK, - ) - - -class IssueLinkViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - - model = IssueLink - serializer_class = IssueLinkSerializer - - def get_queryset(self): - 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")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def create(self, request, slug, project_id, issue_id): - serializer = IssueLinkSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - issue_id=issue_id, - ) - issue_activity.delay( - type="link.activity.created", - requested_data=json.dumps( - serializer.data, cls=DjangoJSONEncoder - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id")), - project_id=str(self.kwargs.get("project_id")), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, issue_id, pk): - issue_link = IssueLink.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) - current_instance = json.dumps( - IssueLinkSerializer(issue_link).data, - cls=DjangoJSONEncoder, - ) - serializer = IssueLinkSerializer( - issue_link, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="link.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, pk): - issue_link = IssueLink.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - pk=pk, - ) - current_instance = json.dumps( - IssueLinkSerializer(issue_link).data, - cls=DjangoJSONEncoder, - ) - issue_activity.delay( - type="link.activity.deleted", - requested_data=json.dumps({"link_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue_link.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - -class BulkCreateIssueLabelsEndpoint(BaseAPIView): - def post(self, request, slug, project_id): - label_data = request.data.get("label_data", []) - project = Project.objects.get(pk=project_id) - - labels = Label.objects.bulk_create( - [ - Label( - name=label.get("name", "Migrated"), - description=label.get("description", "Migrated Issue"), - color="#" + "%06x" % random.randint(0, 0xFFFFFF), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for label in label_data - ], - batch_size=50, - ignore_conflicts=True, - ) - - return Response( - {"labels": LabelSerializer(labels, many=True).data}, - status=status.HTTP_201_CREATED, - ) - - -class IssueAttachmentEndpoint(BaseAPIView): - serializer_class = IssueAttachmentSerializer - permission_classes = [ - ProjectEntityPermission, - ] - model = IssueAttachment - parser_classes = (MultiPartParser, FormParser) - - def post(self, request, slug, project_id, issue_id): - serializer = IssueAttachmentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(project_id=project_id, issue_id=issue_id) - issue_activity.delay( - type="attachment.activity.created", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - serializer.data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, slug, project_id, issue_id, pk): - issue_attachment = IssueAttachment.objects.get(pk=pk) - issue_attachment.asset.delete(save=False) - issue_attachment.delete() - issue_activity.delay( - type="attachment.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=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - - return Response(status=status.HTTP_204_NO_CONTENT) - - def get(self, request, slug, project_id, issue_id): - issue_attachments = IssueAttachment.objects.filter( - issue_id=issue_id, workspace__slug=slug, project_id=project_id - ) - serializer = IssueAttachmentSerializer(issue_attachments, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class IssueArchiveViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - serializer_class = IssueFlatSerializer - model = Issue - - def get_queryset(self): - return ( - Issue.objects.annotate( - sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(archived_at__isnull=False) - .filter(project_id=self.kwargs.get("project_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - ) - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - show_sub_issues = request.GET.get("show_sub_issues", "true") - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - - issue_queryset = ( - issue_queryset - if show_sub_issues == "true" - else issue_queryset.filter(parent__isnull=True) - ) - # Issue queryset - issue_queryset = order_issue_queryset( - issue_queryset=issue_queryset, - order_by_param=order_by_param, - ) - - # Group by - group_by = request.GET.get("group_by", False) - - issue_queryset = issue_queryset_grouper( - queryset=issue_queryset, field=group_by - ) - - # List Paginate - if not group_by: - return self.paginate( - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues - ), - ) - - # Group paginate - return self.paginate( - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues - ), - paginator_cls=GroupedOffsetPaginator, - group_by_field_name=group_by, - count_filter=Q( - Q(issue_inbox__status=1) - | Q(issue_inbox__status=-1) - | Q(issue_inbox__status=2) - | Q(issue_inbox__isnull=True), - archived_at__isnull=False, - is_draft=True, - ), - ) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def archive(self, request, slug, project_id, pk=None): - issue = Issue.issue_objects.get( - workspace__slug=slug, - project_id=project_id, - pk=pk, - ) - if issue.state.group not in ["completed", "cancelled"]: - return Response( - { - "error": "Can only archive completed or cancelled state group issue" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps( - { - "archived_at": str(timezone.now().date()), - "automation": False, - } - ), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue.archived_at = timezone.now().date() - issue.save() - - return Response( - {"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK - ) - - def unarchive(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, - project_id=project_id, - archived_at__isnull=False, - pk=pk, - ) - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"archived_at": None}), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue.archived_at = None - issue.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueSubscriberViewSet(BaseViewSet): - serializer_class = IssueSubscriberSerializer - model = IssueSubscriber - - permission_classes = [ - ProjectEntityPermission, - ] - - def get_permissions(self): - if self.action in ["subscribe", "unsubscribe", "subscription_status"]: - self.permission_classes = [ - ProjectLitePermission, - ] - else: - self.permission_classes = [ - ProjectEntityPermission, - ] - - return super(IssueSubscriberViewSet, self).get_permissions() - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - issue_id=self.kwargs.get("issue_id"), - ) - - def get_queryset(self): - 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")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def list(self, request, slug, project_id, issue_id): - members = ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - is_active=True, - ).select_related("member") - serializer = ProjectMemberLiteSerializer(members, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def destroy(self, request, slug, project_id, issue_id, subscriber_id): - issue_subscriber = IssueSubscriber.objects.get( - project=project_id, - subscriber=subscriber_id, - workspace__slug=slug, - issue=issue_id, - ) - issue_subscriber.delete() - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - - def subscribe(self, request, slug, project_id, issue_id): - if IssueSubscriber.objects.filter( - issue_id=issue_id, - subscriber=request.user, - workspace__slug=slug, - project=project_id, - ).exists(): - return Response( - {"message": "User already subscribed to the issue."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - subscriber = IssueSubscriber.objects.create( - issue_id=issue_id, - subscriber_id=request.user.id, - project_id=project_id, - ) - serializer = IssueSubscriberSerializer(subscriber) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def unsubscribe(self, request, slug, project_id, issue_id): - issue_subscriber = IssueSubscriber.objects.get( - project=project_id, - subscriber=request.user, - workspace__slug=slug, - issue=issue_id, - ) - issue_subscriber.delete() - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - - def subscription_status(self, request, slug, project_id, issue_id): - issue_subscriber = IssueSubscriber.objects.filter( - issue=issue_id, - subscriber=request.user, - workspace__slug=slug, - project=project_id, - ).exists() - return Response( - {"subscribed": issue_subscriber}, status=status.HTTP_200_OK - ) - - -class IssueReactionViewSet(BaseViewSet): - serializer_class = IssueReactionSerializer - model = IssueReaction - permission_classes = [ - ProjectLitePermission, - ] - - def get_queryset(self): - 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")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def create(self, request, slug, project_id, issue_id): - serializer = IssueReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - issue_id=issue_id, - project_id=project_id, - actor=request.user, - ) - issue_activity.delay( - type="issue_reaction.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, reaction_code): - issue_reaction = IssueReaction.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - 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), - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class CommentReactionViewSet(BaseViewSet): - serializer_class = CommentReactionSerializer - model = CommentReaction - permission_classes = [ - ProjectLitePermission, - ] - - def get_queryset(self): - 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")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .order_by("-created_at") - .distinct() - ) - - def create(self, request, slug, project_id, comment_id): - serializer = CommentReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - actor_id=request.user.id, - comment_id=comment_id, - ) - issue_activity.delay( - type="comment_reaction.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=None, - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, comment_id, reaction_code): - comment_reaction = CommentReaction.objects.get( - workspace__slug=slug, - project_id=project_id, - comment_id=comment_id, - 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), - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - comment_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueRelationViewSet(BaseViewSet): - serializer_class = IssueRelationSerializer - model = IssueRelation - permission_classes = [ - ProjectEntityPermission, - ] - - def get_queryset(self): - return self.filter_queryset( - 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")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .distinct() - ) - - def list(self, request, slug, project_id, issue_id): - issue_relations = ( - IssueRelation.objects.filter( - Q(issue_id=issue_id) | Q(related_issue=issue_id) - ) - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .order_by("-created_at") - .distinct() - ) - - blocking_issues = issue_relations.filter( - relation_type="blocked_by", related_issue_id=issue_id - ) - blocked_by_issues = issue_relations.filter( - relation_type="blocked_by", issue_id=issue_id - ) - duplicate_issues = issue_relations.filter( - issue_id=issue_id, relation_type="duplicate" - ) - duplicate_issues_related = issue_relations.filter( - related_issue_id=issue_id, relation_type="duplicate" - ) - relates_to_issues = issue_relations.filter( - issue_id=issue_id, relation_type="relates_to" - ) - relates_to_issues_related = issue_relations.filter( - related_issue_id=issue_id, relation_type="relates_to" - ) - - blocked_by_issues_serialized = IssueRelationSerializer( - blocked_by_issues, many=True - ).data - duplicate_issues_serialized = IssueRelationSerializer( - duplicate_issues, many=True - ).data - relates_to_issues_serialized = IssueRelationSerializer( - relates_to_issues, many=True - ).data - - # revere relation for blocked by issues - blocking_issues_serialized = RelatedIssueSerializer( - blocking_issues, many=True - ).data - # reverse relation for duplicate issues - duplicate_issues_related_serialized = RelatedIssueSerializer( - duplicate_issues_related, many=True - ).data - # reverse relation for related issues - relates_to_issues_related_serialized = RelatedIssueSerializer( - relates_to_issues_related, many=True - ).data - - response_data = { - "blocking": blocking_issues_serialized, - "blocked_by": blocked_by_issues_serialized, - "duplicate": duplicate_issues_serialized - + duplicate_issues_related_serialized, - "relates_to": relates_to_issues_serialized - + relates_to_issues_related_serialized, - } - - return Response(response_data, status=status.HTTP_200_OK) - - def create(self, request, slug, project_id, issue_id): - relation_type = request.data.get("relation_type", None) - issues = request.data.get("issues", []) - project = Project.objects.get(pk=project_id) - - issue_relation = IssueRelation.objects.bulk_create( - [ - IssueRelation( - issue_id=( - issue if relation_type == "blocking" else issue_id - ), - related_issue_id=( - issue_id if relation_type == "blocking" else issue - ), - relation_type=( - "blocked_by" - if relation_type == "blocking" - else relation_type - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for issue in issues - ], - batch_size=10, - ignore_conflicts=True, - ) - - issue_activity.delay( - type="issue_relation.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - - if relation_type == "blocking": - return Response( - RelatedIssueSerializer(issue_relation, many=True).data, - status=status.HTTP_201_CREATED, - ) - else: - return Response( - IssueRelationSerializer(issue_relation, many=True).data, - status=status.HTTP_201_CREATED, - ) - - def remove_relation(self, request, slug, project_id, issue_id): - relation_type = request.data.get("relation_type", None) - related_issue = request.data.get("related_issue", None) - - if relation_type == "blocking": - issue_relation = IssueRelation.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=related_issue, - related_issue_id=issue_id, - ) - else: - issue_relation = IssueRelation.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - related_issue_id=related_issue, - ) - current_instance = json.dumps( - IssueRelationSerializer(issue_relation).data, - cls=DjangoJSONEncoder, - ) - issue_relation.delete() - issue_activity.delay( - type="issue_relation.activity.deleted", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=current_instance, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueDraftViewSet(BaseViewSet): - permission_classes = [ - ProjectEntityPermission, - ] - serializer_class = IssueFlatSerializer - model = Issue - - def get_queryset(self): - return ( - Issue.objects.filter(project_id=self.kwargs.get("project_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(is_draft=True) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - filters = issue_filters(request.query_params, "GET") - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - - # Issue queryset - issue_queryset = order_issue_queryset( - issue_queryset=issue_queryset, - order_by_param=order_by_param, - ) - - # Group by - group_by = request.GET.get("group_by", False) - - issue_queryset = issue_queryset_grouper( - queryset=issue_queryset, field=group_by - ) - - # List Paginate - if not group_by: - return self.paginate( - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues - ), - ) - - # Group paginate - return self.paginate( - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues - ), - paginator_cls=GroupedOffsetPaginator, - group_by_field_name=group_by, - count_filter=Q( - Q(issue_inbox__status=1) - | Q(issue_inbox__status=-1) - | Q(issue_inbox__status=2) - | Q(issue_inbox__isnull=True), - archived_at__isnull=False, - is_draft=True, - ), - ) - - def create(self, request, slug, project_id): - project = Project.objects.get(pk=project_id) - - serializer = IssueCreateSerializer( - data=request.data, - context={ - "project_id": project_id, - "workspace_id": project.workspace_id, - "default_assignee_id": project.default_assignee_id, - }, - ) - - if serializer.is_valid(): - serializer.save(is_draft=True) - - # Track the issue - issue_activity.delay( - type="issue_draft.activity.created", - requested_data=json.dumps( - self.request.data, cls=DjangoJSONEncoder - ), - actor_id=str(request.user.id), - issue_id=str(serializer.data.get("id", None)), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - issue = ( - self.get_queryset().filter(pk=serializer.data["id"]).first() - ) - return Response( - IssueSerializer(issue).data, status=status.HTTP_201_CREATED - ) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, pk): - issue = self.get_queryset().filter(pk=pk).first() - - if not issue: - return Response( - {"error": "Issue does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - - serializer = IssueCreateSerializer( - issue, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="issue_draft.activity.updated", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueSerializer(issue).data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def retrieve(self, request, slug, project_id, pk=None): - issue = ( - self.get_queryset() - .filter(pk=pk) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), - ) - ) - .prefetch_related( - Prefetch( - "issue_attachment", - queryset=IssueAttachment.objects.select_related("issue"), - ) - ) - .prefetch_related( - Prefetch( - "issue_link", - queryset=IssueLink.objects.select_related("created_by"), - ) - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=OuterRef("pk"), - subscriber=request.user, - ) - ) - ) - ).first() - - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - serializer = IssueDetailSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) - - def destroy(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - issue.delete() - issue_activity.delay( - type="issue_draft.activity.deleted", - requested_data=json.dumps({"issue_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(pk), - project_id=str(project_id), - current_instance={}, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/activity.py b/apiserver/plane/app/views/issue/activity.py new file mode 100644 index 000000000..ea6e9b389 --- /dev/null +++ b/apiserver/plane/app/views/issue/activity.py @@ -0,0 +1,85 @@ +# Python imports +from itertools import chain + +# Django imports +from django.db.models import ( + Prefetch, + Q, +) +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseAPIView +from plane.app.serializers import ( + IssueActivitySerializer, + IssueCommentSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + IssueActivity, + IssueComment, + CommentReaction, +) + + +class IssueActivityEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + @method_decorator(gzip_page) + def get(self, request, slug, project_id, issue_id): + filters = {} + if request.GET.get("created_at__gt", None) is not None: + filters = {"created_at__gt": request.GET.get("created_at__gt")} + + issue_activities = ( + IssueActivity.objects.filter(issue_id=issue_id) + .filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + ) + .filter(**filters) + .select_related("actor", "workspace", "issue", "project") + ).order_by("created_at") + issue_comments = ( + IssueComment.objects.filter(issue_id=issue_id) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + workspace__slug=slug, + ) + .filter(**filters) + .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 + + if request.GET.get("activity_type", None) == "issue-property": + return Response(issue_activities, status=status.HTTP_200_OK) + + if request.GET.get("activity_type", None) == "issue-comment": + return Response(issue_comments, status=status.HTTP_200_OK) + + result_list = sorted( + chain(issue_activities, issue_comments), + key=lambda instance: instance["created_at"], + ) + + return Response(result_list, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py new file mode 100644 index 000000000..540715a24 --- /dev/null +++ b/apiserver/plane/app/views/issue/archive.py @@ -0,0 +1,347 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Case, + Value, + CharField, + When, + Exists, + Max, + UUIDField, +) +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueSerializer, + IssueFlatSerializer, + IssueDetailSerializer, +) +from plane.app.permissions import ( + ProjectEntityPermission, +) +from plane.db.models import ( + Issue, + IssueLink, + IssueAttachment, + IssueSubscriber, + IssueReaction, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class IssueArchiveViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + serializer_class = IssueFlatSerializer + model = Issue + + def get_queryset(self): + return ( + Issue.objects.annotate( + sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(archived_at__isnull=False) + .filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ) + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + show_sub_issues = request.GET.get("show_sub_issues", "true") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) + + # 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) + + issue_queryset = ( + issue_queryset + if show_sub_issues == "true" + else issue_queryset.filter(parent__isnull=True) + ) + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def archive(self, request, slug, project_id, pk=None): + issue = Issue.issue_objects.get( + workspace__slug=slug, + project_id=project_id, + pk=pk, + ) + if issue.state.group not in ["completed", "cancelled"]: + return Response( + { + "error": "Can only archive completed or cancelled state group issue" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps( + { + "archived_at": str(timezone.now().date()), + "automation": False, + } + ), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue.archived_at = timezone.now().date() + issue.save() + + return Response( + {"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK + ) + + def unarchive(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, + project_id=project_id, + archived_at__isnull=False, + pk=pk, + ) + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"archived_at": None}), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue.archived_at = None + issue.save() + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/attachment.py b/apiserver/plane/app/views/issue/attachment.py new file mode 100644 index 000000000..c2b8ad6ff --- /dev/null +++ b/apiserver/plane/app/views/issue/attachment.py @@ -0,0 +1,73 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.parsers import MultiPartParser, FormParser + +# Module imports +from .. import BaseAPIView +from plane.app.serializers import IssueAttachmentSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import IssueAttachment +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueAttachmentEndpoint(BaseAPIView): + serializer_class = IssueAttachmentSerializer + permission_classes = [ + ProjectEntityPermission, + ] + model = IssueAttachment + parser_classes = (MultiPartParser, FormParser) + + def post(self, request, slug, project_id, issue_id): + serializer = IssueAttachmentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id, issue_id=issue_id) + issue_activity.delay( + type="attachment.activity.created", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + serializer.data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, slug, project_id, issue_id, pk): + issue_attachment = IssueAttachment.objects.get(pk=pk) + issue_attachment.asset.delete(save=False) + issue_attachment.delete() + issue_activity.delay( + type="attachment.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=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + return Response(status=status.HTTP_204_NO_CONTENT) + + def get(self, request, slug, project_id, issue_id): + issue_attachments = IssueAttachment.objects.filter( + issue_id=issue_id, workspace__slug=slug, project_id=project_id + ) + serializer = IssueAttachmentSerializer(issue_attachments, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py new file mode 100644 index 000000000..63d4358b0 --- /dev/null +++ b/apiserver/plane/app/views/issue/base.py @@ -0,0 +1,686 @@ +# Python imports +import json +import random +from itertools import chain + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Case, + Value, + CharField, + When, + Exists, + Max, +) +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 import IntegrityError +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import Value, UUIDField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.parsers import MultiPartParser, FormParser + +# Module imports +from .. import BaseViewSet, BaseAPIView, WebhookMixin +from plane.app.serializers import ( + IssueActivitySerializer, + IssueCommentSerializer, + IssuePropertySerializer, + IssueSerializer, + IssueCreateSerializer, + LabelSerializer, + IssueFlatSerializer, + IssueLinkSerializer, + IssueLiteSerializer, + IssueAttachmentSerializer, + IssueSubscriberSerializer, + ProjectMemberLiteSerializer, + IssueReactionSerializer, + CommentReactionSerializer, + IssueRelationSerializer, + RelatedIssueSerializer, + IssueDetailSerializer, +) +from plane.app.permissions import ( + ProjectEntityPermission, + WorkSpaceAdminPermission, + ProjectMemberPermission, + ProjectLitePermission, +) +from plane.db.models import ( + Project, + Issue, + IssueActivity, + IssueComment, + IssueProperty, + Label, + IssueLink, + IssueAttachment, + IssueSubscriber, + ProjectMember, + IssueReaction, + CommentReaction, + IssueRelation, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.grouper import group_results +from plane.utils.issue_filters import issue_filters +from collections import defaultdict +from plane.utils.cache import invalidate_cache + +class IssueListEndpoint(BaseAPIView): + + permission_classes = [ + ProjectEntityPermission, + ] + + def get(self, request, slug, project_id): + issue_ids = request.GET.get("issues", False) + + if not issue_ids: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + issue_ids = [ + issue_id for issue_id in issue_ids.split(",") if issue_id != "" + ] + + queryset = ( + Issue.issue_objects.filter( + workspace__slug=slug, project_id=project_id, pk__in=issue_ids + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + 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 = queryset.filter(**filters) + + # 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) + + if self.fields or self.expand: + issues = IssueSerializer( + queryset, many=True, fields=self.fields, expand=self.expand + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + +class IssueViewSet(WebhookMixin, BaseViewSet): + def get_serializer_class(self): + return ( + IssueCreateSerializer + if self.action in ["create", "update", "partial_update"] + else IssueSerializer + ) + + model = Issue + webhook_event = "issue" + permission_classes = [ + ProjectEntityPermission, + ] + + search_fields = [ + "name", + ] + + filterset_fields = [ + "state__name", + "assignees__id", + "workspace__id", + ] + + def get_queryset(self): + return ( + Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id") + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + # 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) + + # Only use serializer when expand or fields else return by values + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + expand=self.expand, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id): + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save() + + # Track the issue + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + .first() + ) + return Response(issue, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, slug, project_id, pk=None): + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + current_instance = json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ) + + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue = self.get_queryset().filter(pk=pk).first() + return Response(status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + issue.delete() + issue_activity.delay( + type="issue.activity.deleted", + requested_data=json.dumps({"issue_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance={}, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class IssueUserDisplayPropertyEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] + + def patch(self, request, slug, project_id): + issue_property = IssueProperty.objects.get( + user=request.user, + project_id=project_id, + ) + + issue_property.filters = request.data.get( + "filters", issue_property.filters + ) + issue_property.display_filters = request.data.get( + "display_filters", issue_property.display_filters + ) + issue_property.display_properties = request.data.get( + "display_properties", issue_property.display_properties + ) + issue_property.save() + serializer = IssuePropertySerializer(issue_property) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug, project_id): + issue_property, _ = IssueProperty.objects.get_or_create( + user=request.user, project_id=project_id + ) + serializer = IssuePropertySerializer(issue_property) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class BulkDeleteIssuesEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + def delete(self, request, slug, project_id): + issue_ids = request.data.get("issue_ids", []) + + if not len(issue_ids): + return Response( + {"error": "Issue IDs are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + issues = Issue.issue_objects.filter( + workspace__slug=slug, project_id=project_id, pk__in=issue_ids + ) + + total_issues = len(issues) + + issues.delete() + + return Response( + {"message": f"{total_issues} issues were deleted"}, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py new file mode 100644 index 000000000..eb2d5834c --- /dev/null +++ b/apiserver/plane/app/views/issue/comment.py @@ -0,0 +1,219 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import Exists +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, WebhookMixin +from plane.app.serializers import ( + IssueCommentSerializer, + CommentReactionSerializer, +) +from plane.app.permissions import ProjectLitePermission +from plane.db.models import ( + IssueComment, + ProjectMember, + CommentReaction, +) +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueCommentViewSet(WebhookMixin, BaseViewSet): + serializer_class = IssueCommentSerializer + model = IssueComment + webhook_event = "issue_comment" + permission_classes = [ + ProjectLitePermission, + ] + + filterset_fields = [ + "issue__id", + "workspace__id", + ] + + def get_queryset(self): + return self.filter_queryset( + 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")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .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, + is_active=True, + ) + ) + ) + .distinct() + ) + + def create(self, request, slug, project_id, issue_id): + serializer = IssueCommentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + actor=request.user, + ) + issue_activity.delay( + type="comment.activity.created", + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id")), + project_id=str(self.kwargs.get("project_id")), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, issue_id, pk): + issue_comment = IssueComment.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + IssueCommentSerializer(issue_comment).data, + cls=DjangoJSONEncoder, + ) + serializer = IssueCommentSerializer( + issue_comment, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="comment.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, pk): + issue_comment = IssueComment.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + current_instance = json.dumps( + IssueCommentSerializer(issue_comment).data, + cls=DjangoJSONEncoder, + ) + issue_comment.delete() + issue_activity.delay( + type="comment.activity.deleted", + requested_data=json.dumps({"comment_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class CommentReactionViewSet(BaseViewSet): + serializer_class = CommentReactionSerializer + model = CommentReaction + permission_classes = [ + ProjectLitePermission, + ] + + def get_queryset(self): + 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")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def create(self, request, slug, project_id, comment_id): + serializer = CommentReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + actor_id=request.user.id, + comment_id=comment_id, + ) + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=None, + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, comment_id, reaction_code): + comment_reaction = CommentReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + comment_id=comment_id, + 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), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + comment_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py new file mode 100644 index 000000000..08032934b --- /dev/null +++ b/apiserver/plane/app/views/issue/draft.py @@ -0,0 +1,367 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Case, + Value, + CharField, + When, + Exists, + Max, + UUIDField, +) +from django.core.serializers.json import DjangoJSONEncoder +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueSerializer, + IssueCreateSerializer, + IssueFlatSerializer, + IssueDetailSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Project, + Issue, + IssueLink, + IssueAttachment, + IssueSubscriber, + IssueReaction, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class IssueDraftViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + serializer_class = IssueFlatSerializer + model = Issue + + def get_queryset(self): + return ( + Issue.objects.filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(is_draft=True) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + + # 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 = self.get_queryset().filter(**filters) + + # 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) + + # Only use serializer when expand else return by values + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + expand=self.expand, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id): + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save(is_draft=True) + + # Track the issue + issue_activity.delay( + type="issue_draft.activity.created", + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue = ( + self.get_queryset().filter(pk=serializer.data["id"]).first() + ) + return Response( + IssueSerializer(issue).data, status=status.HTTP_201_CREATED + ) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, pk): + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="issue_draft.activity.updated", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + IssueSerializer(issue).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + issue.delete() + issue_activity.delay( + type="issue_draft.activity.deleted", + requested_data=json.dumps({"issue_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance={}, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/label.py b/apiserver/plane/app/views/issue/label.py new file mode 100644 index 000000000..557c2018f --- /dev/null +++ b/apiserver/plane/app/views/issue/label.py @@ -0,0 +1,105 @@ +# Python imports +import random + +# Django imports +from django.db import IntegrityError + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, BaseAPIView +from plane.app.serializers import LabelSerializer +from plane.app.permissions import ( + ProjectMemberPermission, +) +from plane.db.models import ( + Project, + Label, +) +from plane.utils.cache import invalidate_cache + + +class LabelViewSet(BaseViewSet): + serializer_class = LabelSerializer + model = Label + permission_classes = [ + ProjectMemberPermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(project__project_projectmember__member=self.request.user) + .select_related("project") + .select_related("workspace") + .select_related("parent") + .distinct() + .order_by("sort_order") + ) + + @invalidate_cache( + path="/api/workspaces/:slug/labels/", url_params=True, user=False + ) + def create(self, request, slug, project_id): + try: + serializer = LabelSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + except IntegrityError: + return Response( + { + "error": "Label with the same name already exists in the project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + @invalidate_cache( + path="/api/workspaces/:slug/labels/", url_params=True, user=False + ) + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache( + path="/api/workspaces/:slug/labels/", url_params=True, user=False + ) + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) + + +class BulkCreateIssueLabelsEndpoint(BaseAPIView): + def post(self, request, slug, project_id): + label_data = request.data.get("label_data", []) + project = Project.objects.get(pk=project_id) + + labels = Label.objects.bulk_create( + [ + Label( + name=label.get("name", "Migrated"), + description=label.get("description", "Migrated Issue"), + color="#" + "%06x" % random.randint(0, 0xFFFFFF), + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for label in label_data + ], + batch_size=50, + ignore_conflicts=True, + ) + + return Response( + {"labels": LabelSerializer(labels, many=True).data}, + status=status.HTTP_201_CREATED, + ) diff --git a/apiserver/plane/app/views/issue/link.py b/apiserver/plane/app/views/issue/link.py new file mode 100644 index 000000000..ca3290759 --- /dev/null +++ b/apiserver/plane/app/views/issue/link.py @@ -0,0 +1,120 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import IssueLinkSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import IssueLink +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueLinkViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + + model = IssueLink + serializer_class = IssueLinkSerializer + + def get_queryset(self): + 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")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def create(self, request, slug, project_id, issue_id): + serializer = IssueLinkSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + ) + issue_activity.delay( + type="link.activity.created", + requested_data=json.dumps( + serializer.data, cls=DjangoJSONEncoder + ), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id")), + project_id=str(self.kwargs.get("project_id")), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, issue_id, pk): + issue_link = IssueLink.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + IssueLinkSerializer(issue_link).data, + cls=DjangoJSONEncoder, + ) + serializer = IssueLinkSerializer( + issue_link, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="link.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, pk): + issue_link = IssueLink.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + pk=pk, + ) + current_instance = json.dumps( + IssueLinkSerializer(issue_link).data, + cls=DjangoJSONEncoder, + ) + issue_activity.delay( + type="link.activity.deleted", + requested_data=json.dumps({"link_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue_link.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/reaction.py b/apiserver/plane/app/views/issue/reaction.py new file mode 100644 index 000000000..c6f6823be --- /dev/null +++ b/apiserver/plane/app/views/issue/reaction.py @@ -0,0 +1,89 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import IssueReactionSerializer +from plane.app.permissions import ProjectLitePermission +from plane.db.models import IssueReaction +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueReactionViewSet(BaseViewSet): + serializer_class = IssueReactionSerializer + model = IssueReaction + permission_classes = [ + ProjectLitePermission, + ] + + def get_queryset(self): + 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")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def create(self, request, slug, project_id, issue_id): + serializer = IssueReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + issue_id=issue_id, + project_id=project_id, + actor=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, reaction_code): + issue_reaction = IssueReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + 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), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/relation.py b/apiserver/plane/app/views/issue/relation.py new file mode 100644 index 000000000..45a5dc9a7 --- /dev/null +++ b/apiserver/plane/app/views/issue/relation.py @@ -0,0 +1,204 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import Q +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueRelationSerializer, + RelatedIssueSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Project, + IssueRelation, +) +from plane.bgtasks.issue_activites_task import issue_activity + + +class IssueRelationViewSet(BaseViewSet): + serializer_class = IssueRelationSerializer + model = IssueRelation + permission_classes = [ + ProjectEntityPermission, + ] + + def get_queryset(self): + return self.filter_queryset( + 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")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .distinct() + ) + + def list(self, request, slug, project_id, issue_id): + issue_relations = ( + IssueRelation.objects.filter( + Q(issue_id=issue_id) | Q(related_issue=issue_id) + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .order_by("-created_at") + .distinct() + ) + + blocking_issues = issue_relations.filter( + relation_type="blocked_by", related_issue_id=issue_id + ) + blocked_by_issues = issue_relations.filter( + relation_type="blocked_by", issue_id=issue_id + ) + duplicate_issues = issue_relations.filter( + issue_id=issue_id, relation_type="duplicate" + ) + duplicate_issues_related = issue_relations.filter( + related_issue_id=issue_id, relation_type="duplicate" + ) + relates_to_issues = issue_relations.filter( + issue_id=issue_id, relation_type="relates_to" + ) + relates_to_issues_related = issue_relations.filter( + related_issue_id=issue_id, relation_type="relates_to" + ) + + blocked_by_issues_serialized = IssueRelationSerializer( + blocked_by_issues, many=True + ).data + duplicate_issues_serialized = IssueRelationSerializer( + duplicate_issues, many=True + ).data + relates_to_issues_serialized = IssueRelationSerializer( + relates_to_issues, many=True + ).data + + # revere relation for blocked by issues + blocking_issues_serialized = RelatedIssueSerializer( + blocking_issues, many=True + ).data + # reverse relation for duplicate issues + duplicate_issues_related_serialized = RelatedIssueSerializer( + duplicate_issues_related, many=True + ).data + # reverse relation for related issues + relates_to_issues_related_serialized = RelatedIssueSerializer( + relates_to_issues_related, many=True + ).data + + response_data = { + "blocking": blocking_issues_serialized, + "blocked_by": blocked_by_issues_serialized, + "duplicate": duplicate_issues_serialized + + duplicate_issues_related_serialized, + "relates_to": relates_to_issues_serialized + + relates_to_issues_related_serialized, + } + + return Response(response_data, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id, issue_id): + relation_type = request.data.get("relation_type", None) + issues = request.data.get("issues", []) + project = Project.objects.get(pk=project_id) + + issue_relation = IssueRelation.objects.bulk_create( + [ + IssueRelation( + issue_id=( + issue if relation_type == "blocking" else issue_id + ), + related_issue_id=( + issue_id if relation_type == "blocking" else issue + ), + relation_type=( + "blocked_by" + if relation_type == "blocking" + else relation_type + ), + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for issue in issues + ], + batch_size=10, + ignore_conflicts=True, + ) + + issue_activity.delay( + type="issue_relation.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + if relation_type == "blocking": + return Response( + RelatedIssueSerializer(issue_relation, many=True).data, + status=status.HTTP_201_CREATED, + ) + else: + return Response( + IssueRelationSerializer(issue_relation, many=True).data, + status=status.HTTP_201_CREATED, + ) + + def remove_relation(self, request, slug, project_id, issue_id): + relation_type = request.data.get("relation_type", None) + related_issue = request.data.get("related_issue", None) + + if relation_type == "blocking": + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=related_issue, + related_issue_id=issue_id, + ) + else: + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + related_issue_id=related_issue, + ) + current_instance = json.dumps( + IssueRelationSerializer(issue_relation).data, + cls=DjangoJSONEncoder, + ) + issue_relation.delete() + issue_activity.delay( + type="issue_relation.activity.deleted", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/sub_issue.py b/apiserver/plane/app/views/issue/sub_issue.py new file mode 100644 index 000000000..6ec4a2de1 --- /dev/null +++ b/apiserver/plane/app/views/issue/sub_issue.py @@ -0,0 +1,195 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + OuterRef, + Func, + F, + Q, + Value, + UUIDField, +) +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseAPIView +from plane.app.serializers import IssueSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + Issue, + IssueLink, + IssueAttachment, +) +from plane.bgtasks.issue_activites_task import issue_activity +from collections import defaultdict + + +class SubIssuesEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + @method_decorator(gzip_page) + def get(self, request, slug, project_id, issue_id): + sub_issues = ( + Issue.issue_objects.filter( + parent_id=issue_id, workspace__slug=slug + ) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .annotate(state_group=F("state__group")) + ) + + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) + + sub_issues = sub_issues.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response( + { + "sub_issues": sub_issues, + "state_distribution": result, + }, + status=status.HTTP_200_OK, + ) + + # Assign multiple sub issues + def post(self, request, slug, project_id, issue_id): + parent_issue = Issue.issue_objects.get(pk=issue_id) + sub_issue_ids = request.data.get("sub_issue_ids", []) + + if not len(sub_issue_ids): + return Response( + {"error": "Sub Issue IDs are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) + + for sub_issue in sub_issues: + sub_issue.parent = parent_issue + + _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) + + updated_sub_issues = Issue.issue_objects.filter( + id__in=sub_issue_ids + ).annotate(state_group=F("state__group")) + + # Track the issue + _ = [ + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"parent": str(issue_id)}), + actor_id=str(request.user.id), + issue_id=str(sub_issue_id), + project_id=str(project_id), + current_instance=json.dumps({"parent": str(sub_issue_id)}), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for sub_issue_id in sub_issue_ids + ] + + # create's a dict with state group name with their respective issue id's + result = defaultdict(list) + for sub_issue in updated_sub_issues: + result[sub_issue.state_group].append(str(sub_issue.id)) + + serializer = IssueSerializer( + updated_sub_issues, + many=True, + ) + return Response( + { + "sub_issues": serializer.data, + "state_distribution": result, + }, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/app/views/issue/subscriber.py b/apiserver/plane/app/views/issue/subscriber.py new file mode 100644 index 000000000..61e09e4a2 --- /dev/null +++ b/apiserver/plane/app/views/issue/subscriber.py @@ -0,0 +1,124 @@ +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet +from plane.app.serializers import ( + IssueSubscriberSerializer, + ProjectMemberLiteSerializer, +) +from plane.app.permissions import ( + ProjectEntityPermission, + ProjectLitePermission, +) +from plane.db.models import ( + IssueSubscriber, + ProjectMember, +) + + +class IssueSubscriberViewSet(BaseViewSet): + serializer_class = IssueSubscriberSerializer + model = IssueSubscriber + + permission_classes = [ + ProjectEntityPermission, + ] + + def get_permissions(self): + if self.action in ["subscribe", "unsubscribe", "subscription_status"]: + self.permission_classes = [ + ProjectLitePermission, + ] + else: + self.permission_classes = [ + ProjectEntityPermission, + ] + + return super(IssueSubscriberViewSet, self).get_permissions() + + def perform_create(self, serializer): + serializer.save( + project_id=self.kwargs.get("project_id"), + issue_id=self.kwargs.get("issue_id"), + ) + + def get_queryset(self): + 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")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .order_by("-created_at") + .distinct() + ) + + def list(self, request, slug, project_id, issue_id): + members = ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + is_active=True, + ).select_related("member") + serializer = ProjectMemberLiteSerializer(members, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, slug, project_id, issue_id, subscriber_id): + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=subscriber_id, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + + def subscribe(self, request, slug, project_id, issue_id): + if IssueSubscriber.objects.filter( + issue_id=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists(): + return Response( + {"message": "User already subscribed to the issue."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + subscriber = IssueSubscriber.objects.create( + issue_id=issue_id, + subscriber_id=request.user.id, + project_id=project_id, + ) + serializer = IssueSubscriberSerializer(subscriber) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def unsubscribe(self, request, slug, project_id, issue_id): + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=request.user, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + + def subscription_status(self, request, slug, project_id, issue_id): + issue_subscriber = IssueSubscriber.objects.filter( + issue=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists() + return Response( + {"subscribed": issue_subscriber}, status=status.HTTP_200_OK + ) diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module/base.py similarity index 98% rename from apiserver/plane/app/views/module.py rename to apiserver/plane/app/views/module/base.py index 97644bec8..250cf3719 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module/base.py @@ -2,53 +2,63 @@ import json # Django Imports -from django.utils import timezone -from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q -from django.utils.decorators import method_decorator -from django.views.decorators.gzip import gzip_page from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField -from django.db.models import Value, UUIDField +from django.db.models import ( + Count, + Exists, + F, + Func, + OuterRef, + Prefetch, + Q, + UUIDField, + Value, +) from django.db.models.functions import Coalesce +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page # Third party imports -from rest_framework.response import Response from rest_framework import status +from rest_framework.response import Response -# Module imports -from . import BaseViewSet, BaseAPIView, WebhookMixin -from plane.app.serializers import ( - ModuleWriteSerializer, - ModuleSerializer, - ModuleIssueSerializer, - ModuleLinkSerializer, - ModuleFavoriteSerializer, - IssueSerializer, - ModuleUserPropertiesSerializer, - ModuleDetailSerializer, -) from plane.app.permissions import ( ProjectEntityPermission, ProjectLitePermission, ) -from plane.db.models import ( - Module, - ModuleIssue, - Project, - Issue, - ModuleLink, - ModuleFavorite, - IssueLink, - IssueAttachment, - ModuleUserProperties, +from plane.app.serializers import ( + ModuleDetailSerializer, + ModuleFavoriteSerializer, + ModuleIssueSerializer, + ModuleLinkSerializer, + ModuleSerializer, + ModuleUserPropertiesSerializer, + ModuleWriteSerializer, ) from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.issue_filters import issue_filters +from plane.db.models import ( + Issue, + IssueAttachment, + IssueLink, + Module, + ModuleFavorite, + ModuleIssue, + ModuleLink, + ModuleUserProperties, + Project, +) from plane.utils.analytics_plot import burndown_plot -from plane.utils.grouper import issue_queryset_grouper, issue_on_results +from plane.utils.grouper import issue_on_results, issue_queryset_grouper +from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset from plane.utils.paginator import GroupedOffsetPaginator +# Module imports +from .. import BaseAPIView, BaseViewSet, WebhookMixin + + class ModuleViewSet(WebhookMixin, BaseViewSet): model = Module permission_classes = [ @@ -368,7 +378,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): if serializer.is_valid(): serializer.save() - module = queryset.values( + module = queryset.values( # Required fields "id", "workspace_id", @@ -524,7 +534,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): ), ) - # create multiple issues inside a module def create_module_issues(self, request, slug, project_id, module_id): issues = request.data.get("issues", []) diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py new file mode 100644 index 000000000..cfa8ee478 --- /dev/null +++ b/apiserver/plane/app/views/module/issue.py @@ -0,0 +1,259 @@ +# Python imports +import json + +# Django Imports +from django.utils import timezone +from django.db.models import F, OuterRef, Func, Q +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import Value, UUIDField +from django.db.models.functions import Coalesce + +# Third party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .. import BaseViewSet, WebhookMixin +from plane.app.serializers import ( + ModuleIssueSerializer, + IssueSerializer, +) +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import ( + ModuleIssue, + Project, + Issue, + IssueLink, + IssueAttachment, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.issue_filters import issue_filters + + +class ModuleIssueViewSet(WebhookMixin, BaseViewSet): + serializer_class = ModuleIssueSerializer + model = ModuleIssue + webhook_event = "module_issue" + bulk = True + + filterset_fields = [ + "issue__labels__id", + "issue__assignees__id", + ] + + permission_classes = [ + ProjectEntityPermission, + ] + + def get_queryset(self): + return ( + Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + issue_module__module_id=self.kwargs.get("module_id"), + ) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id, module_id): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + filters = issue_filters(request.query_params, "GET") + issue_queryset = self.get_queryset().filter(**filters) + if self.fields or self.expand: + issues = IssueSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) + + # create multiple issues inside a module + def create_module_issues(self, request, slug, project_id, module_id): + issues = request.data.get("issues", []) + if not issues: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + project = Project.objects.get(pk=project_id) + _ = ModuleIssue.objects.bulk_create( + [ + ModuleIssue( + issue_id=str(issue), + module_id=module_id, + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for issue in issues + ], + batch_size=10, + ignore_conflicts=True, + ) + # Bulk Update the activity + _ = [ + issue_activity.delay( + type="module.activity.created", + requested_data=json.dumps({"module_id": str(module_id)}), + actor_id=str(request.user.id), + issue_id=str(issue), + project_id=project_id, + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for issue in issues + ] + return Response({"message": "success"}, status=status.HTTP_201_CREATED) + + # create multiple module inside an issue + def create_issue_modules(self, request, slug, project_id, issue_id): + modules = request.data.get("modules", []) + if not modules: + return Response( + {"error": "Modules are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + project = Project.objects.get(pk=project_id) + _ = ModuleIssue.objects.bulk_create( + [ + ModuleIssue( + issue_id=issue_id, + module_id=module, + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for module in modules + ], + batch_size=10, + ignore_conflicts=True, + ) + # Bulk Update the activity + _ = [ + issue_activity.delay( + type="module.activity.created", + requested_data=json.dumps({"module_id": module}), + actor_id=str(request.user.id), + issue_id=issue_id, + project_id=project_id, + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for module in modules + ] + + return Response({"message": "success"}, status=status.HTTP_201_CREATED) + + def destroy(self, request, slug, project_id, module_id, issue_id): + module_issue = ModuleIssue.objects.get( + workspace__slug=slug, + project_id=project_id, + module_id=module_id, + issue_id=issue_id, + ) + issue_activity.delay( + type="module.activity.deleted", + requested_data=json.dumps({"module_id": str(module_id)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=json.dumps( + {"module_name": module_issue.module.name} + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + module_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/notification.py b/apiserver/plane/app/views/notification/base.py similarity index 98% rename from apiserver/plane/app/views/notification.py rename to apiserver/plane/app/views/notification/base.py index ebe8e5082..8dae618db 100644 --- a/apiserver/plane/app/views/notification.py +++ b/apiserver/plane/app/views/notification/base.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from plane.utils.paginator import BasePaginator # Module imports -from .base import BaseViewSet, BaseAPIView +from ..base import BaseViewSet, BaseAPIView from plane.db.models import ( Notification, IssueAssignee, @@ -17,7 +17,10 @@ from plane.db.models import ( WorkspaceMember, UserNotificationPreference, ) -from plane.app.serializers import NotificationSerializer, UserNotificationPreferenceSerializer +from plane.app.serializers import ( + NotificationSerializer, + UserNotificationPreferenceSerializer, +) class NotificationViewSet(BaseViewSet, BasePaginator): diff --git a/apiserver/plane/app/views/oauth.py b/apiserver/plane/app/views/oauth.py index 8152fb0ee..48630175a 100644 --- a/apiserver/plane/app/views/oauth.py +++ b/apiserver/plane/app/views/oauth.py @@ -5,7 +5,6 @@ import os # Django imports from django.utils import timezone -from django.conf import settings # Third Party modules from rest_framework.response import Response @@ -250,9 +249,11 @@ class OauthEndpoint(BaseAPIView): [ WorkspaceMember( workspace_id=project_member_invite.workspace_id, - role=project_member_invite.role - if project_member_invite.role in [5, 10, 15] - else 15, + role=( + project_member_invite.role + if project_member_invite.role in [5, 10, 15] + else 15 + ), member=user, created_by_id=project_member_invite.created_by_id, ) @@ -266,9 +267,11 @@ class OauthEndpoint(BaseAPIView): [ ProjectMember( workspace_id=project_member_invite.workspace_id, - role=project_member_invite.role - if project_member_invite.role in [5, 10, 15] - else 15, + role=( + project_member_invite.role + if project_member_invite.role in [5, 10, 15] + else 15 + ), member=user, created_by_id=project_member_invite.created_by_id, ) @@ -391,9 +394,11 @@ class OauthEndpoint(BaseAPIView): [ WorkspaceMember( workspace_id=project_member_invite.workspace_id, - role=project_member_invite.role - if project_member_invite.role in [5, 10, 15] - else 15, + role=( + project_member_invite.role + if project_member_invite.role in [5, 10, 15] + else 15 + ), member=user, created_by_id=project_member_invite.created_by_id, ) @@ -407,9 +412,11 @@ class OauthEndpoint(BaseAPIView): [ ProjectMember( workspace_id=project_member_invite.workspace_id, - role=project_member_invite.role - if project_member_invite.role in [5, 10, 15] - else 15, + role=( + project_member_invite.role + if project_member_invite.role in [5, 10, 15] + else 15 + ), member=user, created_by_id=project_member_invite.created_by_id, ) diff --git a/apiserver/plane/app/views/page.py b/apiserver/plane/app/views/page/base.py similarity index 96% rename from apiserver/plane/app/views/page.py rename to apiserver/plane/app/views/page/base.py index 7ecf22fa8..34a9ee638 100644 --- a/apiserver/plane/app/views/page.py +++ b/apiserver/plane/app/views/page/base.py @@ -1,25 +1,32 @@ # Python imports -from datetime import date, datetime, timedelta +from datetime import datetime # Django imports from django.db import connection from django.db.models import Exists, OuterRef, Q -from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page + # Third party imports from rest_framework import status from rest_framework.response import Response from plane.app.permissions import ProjectEntityPermission -from plane.app.serializers import (IssueLiteSerializer, PageFavoriteSerializer, - PageLogSerializer, PageSerializer, - SubPageSerializer) -from plane.db.models import (Issue, IssueActivity, IssueAssignee, Page, - PageFavorite, PageLog, ProjectMember) +from plane.app.serializers import ( + PageFavoriteSerializer, + PageLogSerializer, + PageSerializer, + SubPageSerializer, +) +from plane.db.models import ( + Page, + PageFavorite, + PageLog, + ProjectMember, +) # Module imports -from .base import BaseAPIView, BaseViewSet +from ..base import BaseAPIView, BaseViewSet def unarchive_archive_page_and_descendants(page_id, archived_at): diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py deleted file mode 100644 index 6f9b2618e..000000000 --- a/apiserver/plane/app/views/project.py +++ /dev/null @@ -1,1139 +0,0 @@ -# Python imports -import jwt -import boto3 -from datetime import datetime - -# Django imports -from django.core.exceptions import ValidationError -from django.db import IntegrityError -from django.db.models import ( - Prefetch, - Q, - Exists, - OuterRef, - F, - Func, - Subquery, -) -from django.core.validators import validate_email -from django.conf import settings -from django.utils import timezone - -# Third Party imports -from rest_framework.response import Response -from rest_framework import status -from rest_framework import serializers -from rest_framework.permissions import AllowAny - -# Module imports -from .base import BaseViewSet, BaseAPIView, WebhookMixin -from plane.app.serializers import ( - ProjectSerializer, - ProjectListSerializer, - ProjectMemberSerializer, - ProjectDetailSerializer, - ProjectMemberInviteSerializer, - ProjectFavoriteSerializer, - ProjectDeployBoardSerializer, - ProjectMemberAdminSerializer, - ProjectMemberRoleSerializer, -) - -from plane.app.permissions import ( - WorkspaceUserPermission, - ProjectBasePermission, - ProjectMemberPermission, - ProjectLitePermission, -) - -from plane.db.models import ( - Project, - ProjectMember, - Workspace, - ProjectMemberInvite, - User, - WorkspaceMember, - State, - TeamMember, - ProjectFavorite, - ProjectIdentifier, - Module, - Cycle, - Inbox, - ProjectDeployBoard, - IssueProperty, -) - -from plane.bgtasks.project_invitation_task import project_invitation - - -class ProjectViewSet(WebhookMixin, BaseViewSet): - serializer_class = ProjectListSerializer - model = Project - webhook_event = "project" - - permission_classes = [ - ProjectBasePermission, - ] - - def get_queryset(self): - sort_order = ProjectMember.objects.filter( - member=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ).values("sort_order") - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter( - Q(project_projectmember__member=self.request.user) - | Q(network=2) - ) - .select_related( - "workspace", - "workspace__owner", - "default_assignee", - "project_lead", - ) - .annotate( - is_favorite=Exists( - ProjectFavorite.objects.filter( - user=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - ) - ) - .annotate( - is_member=Exists( - ProjectMember.objects.filter( - member=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ) - ) - ) - .annotate( - total_members=ProjectMember.objects.filter( - project_id=OuterRef("id"), - member__is_bot=False, - is_active=True, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - total_cycles=Cycle.objects.filter(project_id=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - total_modules=Module.objects.filter(project_id=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - member_role=ProjectMember.objects.filter( - project_id=OuterRef("pk"), - member_id=self.request.user.id, - is_active=True, - ).values("role") - ) - .annotate( - is_deployed=Exists( - ProjectDeployBoard.objects.filter( - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - ) - ) - .annotate(sort_order=Subquery(sort_order)) - .prefetch_related( - Prefetch( - "project_projectmember", - queryset=ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ).select_related("member"), - to_attr="members_list", - ) - ) - .distinct() - ) - - def list(self, request, slug): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - projects = ( - self.get_queryset() - .order_by("sort_order", "name") - ) - if request.GET.get("per_page", False) and request.GET.get( - "cursor", False - ): - return self.paginate( - request=request, - queryset=(projects), - on_results=lambda projects: ProjectListSerializer( - projects, many=True - ).data, - ) - projects = ProjectListSerializer( - projects, many=True, fields=fields if fields else None - ).data - return Response(projects, status=status.HTTP_200_OK) - - def create(self, request, slug): - try: - workspace = Workspace.objects.get(slug=slug) - - serializer = ProjectSerializer( - data={**request.data}, context={"workspace_id": workspace.id} - ) - if serializer.is_valid(): - serializer.save() - - # Add the user as Administrator to the project - _ = ProjectMember.objects.create( - project_id=serializer.data["id"], - member=request.user, - role=20, - ) - # Also create the issue property for the user - _ = IssueProperty.objects.create( - project_id=serializer.data["id"], - user=request.user, - ) - - if serializer.data["project_lead"] is not None and str( - serializer.data["project_lead"] - ) != str(request.user.id): - ProjectMember.objects.create( - project_id=serializer.data["id"], - member_id=serializer.data["project_lead"], - role=20, - ) - # Also create the issue property for the user - IssueProperty.objects.create( - project_id=serializer.data["id"], - user_id=serializer.data["project_lead"], - ) - - # Default states - states = [ - { - "name": "Backlog", - "color": "#A3A3A3", - "sequence": 15000, - "group": "backlog", - "default": True, - }, - { - "name": "Todo", - "color": "#3A3A3A", - "sequence": 25000, - "group": "unstarted", - }, - { - "name": "In Progress", - "color": "#F59E0B", - "sequence": 35000, - "group": "started", - }, - { - "name": "Done", - "color": "#16A34A", - "sequence": 45000, - "group": "completed", - }, - { - "name": "Cancelled", - "color": "#EF4444", - "sequence": 55000, - "group": "cancelled", - }, - ] - - State.objects.bulk_create( - [ - State( - name=state["name"], - color=state["color"], - project=serializer.instance, - sequence=state["sequence"], - workspace=serializer.instance.workspace, - group=state["group"], - default=state.get("default", False), - created_by=request.user, - ) - for state in states - ] - ) - - project = ( - self.get_queryset() - .filter(pk=serializer.data["id"]) - .first() - ) - serializer = ProjectListSerializer(project) - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - serializer.errors, - status=status.HTTP_400_BAD_REQUEST, - ) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"name": "The project name is already taken"}, - status=status.HTTP_410_GONE, - ) - except Workspace.DoesNotExist as e: - return Response( - {"error": "Workspace does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except serializers.ValidationError as e: - return Response( - {"identifier": "The project identifier is already taken"}, - status=status.HTTP_410_GONE, - ) - - def partial_update(self, request, slug, pk=None): - try: - workspace = Workspace.objects.get(slug=slug) - - project = Project.objects.get(pk=pk) - - serializer = ProjectSerializer( - project, - data={**request.data}, - context={"workspace_id": workspace.id}, - partial=True, - ) - - if serializer.is_valid(): - serializer.save() - if serializer.data["inbox_view"]: - Inbox.objects.get_or_create( - name=f"{project.name} Inbox", - project=project, - is_default=True, - ) - - # Create the triage state in Backlog group - State.objects.get_or_create( - name="Triage", - group="backlog", - description="Default state for managing all Inbox Issues", - project_id=pk, - color="#ff7700", - ) - - project = ( - self.get_queryset() - .filter(pk=serializer.data["id"]) - .first() - ) - serializer = ProjectListSerializer(project) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response( - serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"name": "The project name is already taken"}, - status=status.HTTP_410_GONE, - ) - except (Project.DoesNotExist, Workspace.DoesNotExist): - return Response( - {"error": "Project does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except serializers.ValidationError as e: - return Response( - {"identifier": "The project identifier is already taken"}, - status=status.HTTP_410_GONE, - ) - - -class ProjectInvitationsViewset(BaseViewSet): - serializer_class = ProjectMemberInviteSerializer - model = ProjectMemberInvite - - search_fields = [] - - permission_classes = [ - ProjectBasePermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .select_related("project") - .select_related("workspace", "workspace__owner") - ) - - def create(self, request, slug, project_id): - emails = request.data.get("emails", []) - - # Check if email is provided - if not emails: - return Response( - {"error": "Emails are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - requesting_user = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - member_id=request.user.id, - ) - - # Check if any invited user has an higher role - if len( - [ - email - for email in emails - if int(email.get("role", 10)) > requesting_user.role - ] - ): - return Response( - {"error": "You cannot invite a user with higher role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - project_invitations = [] - for email in emails: - try: - validate_email(email.get("email")) - project_invitations.append( - ProjectMemberInvite( - email=email.get("email").strip().lower(), - project_id=project_id, - workspace_id=workspace.id, - token=jwt.encode( - { - "email": email, - "timestamp": datetime.now().timestamp(), - }, - settings.SECRET_KEY, - algorithm="HS256", - ), - role=email.get("role", 10), - created_by=request.user, - ) - ) - except ValidationError: - return Response( - { - "error": f"Invalid email - {email} provided a valid email address is required to send the invite" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Create workspace member invite - project_invitations = ProjectMemberInvite.objects.bulk_create( - project_invitations, batch_size=10, ignore_conflicts=True - ) - current_site = request.META.get("HTTP_ORIGIN") - - # Send invitations - for invitation in project_invitations: - project_invitations.delay( - invitation.email, - project_id, - invitation.token, - current_site, - request.user.email, - ) - - return Response( - { - "message": "Email sent successfully", - }, - status=status.HTTP_200_OK, - ) - - -class UserProjectInvitationsViewset(BaseViewSet): - serializer_class = ProjectMemberInviteSerializer - model = ProjectMemberInvite - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(email=self.request.user.email) - .select_related("workspace", "workspace__owner", "project") - ) - - def create(self, request, slug): - project_ids = request.data.get("project_ids", []) - - # Get the workspace user role - workspace_member = WorkspaceMember.objects.get( - member=request.user, - workspace__slug=slug, - is_active=True, - ) - - workspace_role = workspace_member.role - workspace = workspace_member.workspace - - # If the user was already part of workspace - _ = ProjectMember.objects.filter( - workspace__slug=slug, - project_id__in=project_ids, - member=request.user, - ).update(is_active=True) - - ProjectMember.objects.bulk_create( - [ - ProjectMember( - project_id=project_id, - member=request.user, - role=15 if workspace_role >= 15 else 10, - workspace=workspace, - created_by=request.user, - ) - for project_id in project_ids - ], - ignore_conflicts=True, - ) - - IssueProperty.objects.bulk_create( - [ - IssueProperty( - project_id=project_id, - user=request.user, - workspace=workspace, - created_by=request.user, - ) - for project_id in project_ids - ], - ignore_conflicts=True, - ) - - return Response( - {"message": "Projects joined successfully"}, - status=status.HTTP_201_CREATED, - ) - - -class ProjectJoinEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def post(self, request, slug, project_id, pk): - project_invite = ProjectMemberInvite.objects.get( - pk=pk, - project_id=project_id, - workspace__slug=slug, - ) - - email = request.data.get("email", "") - - if email == "" or project_invite.email != email: - return Response( - {"error": "You do not have permission to join the project"}, - status=status.HTTP_403_FORBIDDEN, - ) - - if project_invite.responded_at is None: - project_invite.accepted = request.data.get("accepted", False) - project_invite.responded_at = timezone.now() - project_invite.save() - - if project_invite.accepted: - # Check if the user account exists - user = User.objects.filter(email=email).first() - - # Check if user is a part of workspace - workspace_member = WorkspaceMember.objects.filter( - workspace__slug=slug, member=user - ).first() - # Add him to workspace - if workspace_member is None: - _ = WorkspaceMember.objects.create( - workspace_id=project_invite.workspace_id, - member=user, - role=15 - if project_invite.role >= 15 - else project_invite.role, - ) - else: - # Else make him active - workspace_member.is_active = True - workspace_member.save() - - # Check if the user was already a member of project then activate the user - project_member = ProjectMember.objects.filter( - workspace_id=project_invite.workspace_id, member=user - ).first() - if project_member is None: - # Create a Project Member - _ = ProjectMember.objects.create( - workspace_id=project_invite.workspace_id, - member=user, - role=project_invite.role, - ) - else: - project_member.is_active = True - project_member.role = project_member.role - project_member.save() - - return Response( - {"message": "Project Invitation Accepted"}, - status=status.HTTP_200_OK, - ) - - return Response( - {"message": "Project Invitation was not accepted"}, - status=status.HTTP_200_OK, - ) - - return Response( - {"error": "You have already responded to the invitation request"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get(self, request, slug, project_id, pk): - project_invitation = ProjectMemberInvite.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - serializer = ProjectMemberInviteSerializer(project_invitation) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class ProjectMemberViewSet(BaseViewSet): - serializer_class = ProjectMemberAdminSerializer - model = ProjectMember - permission_classes = [ - ProjectMemberPermission, - ] - - def get_permissions(self): - if self.action == "leave": - self.permission_classes = [ - ProjectLitePermission, - ] - else: - self.permission_classes = [ - ProjectMemberPermission, - ] - - return super(ProjectMemberViewSet, self).get_permissions() - - search_fields = [ - "member__display_name", - "member__first_name", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(member__is_bot=False) - .filter() - .select_related("project") - .select_related("member") - .select_related("workspace", "workspace__owner") - ) - - def create(self, request, slug, project_id): - members = request.data.get("members", []) - - # get the project - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - if not len(members): - return Response( - {"error": "Atleast one member is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - bulk_project_members = [] - bulk_issue_props = [] - - project_members = ( - ProjectMember.objects.filter( - workspace__slug=slug, - member_id__in=[member.get("member_id") for member in members], - ) - .values("member_id", "sort_order") - .order_by("sort_order") - ) - - bulk_project_members = [] - member_roles = {member.get("member_id"): member.get("role") for member in members} - # Update roles in the members array based on the member_roles dictionary - for project_member in ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]): - project_member.role = member_roles[str(project_member.member_id)] - project_member.is_active = True - bulk_project_members.append(project_member) - - # Update the roles of the existing members - ProjectMember.objects.bulk_update( - bulk_project_members, ["is_active", "role"], batch_size=100 - ) - - for member in members: - sort_order = [ - project_member.get("sort_order") - for project_member in project_members - if str(project_member.get("member_id")) - == str(member.get("member_id")) - ] - bulk_project_members.append( - ProjectMember( - member_id=member.get("member_id"), - role=member.get("role", 10), - project_id=project_id, - workspace_id=project.workspace_id, - sort_order=sort_order[0] - 10000 - if len(sort_order) - else 65535, - ) - ) - bulk_issue_props.append( - IssueProperty( - user_id=member.get("member_id"), - project_id=project_id, - workspace_id=project.workspace_id, - ) - ) - - project_members = ProjectMember.objects.bulk_create( - bulk_project_members, - batch_size=10, - ignore_conflicts=True, - ) - - _ = IssueProperty.objects.bulk_create( - bulk_issue_props, batch_size=10, ignore_conflicts=True - ) - - project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]) - serializer = ProjectMemberRoleSerializer(project_members, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def list(self, request, slug, project_id): - # Get the list of project members for the project - project_members = ProjectMember.objects.filter( - project_id=project_id, - workspace__slug=slug, - member__is_bot=False, - is_active=True, - ).select_related("project", "member", "workspace") - - serializer = ProjectMemberRoleSerializer( - project_members, fields=("id", "member", "role"), many=True - ) - return Response(serializer.data, status=status.HTTP_200_OK) - - def partial_update(self, request, slug, project_id, pk): - project_member = ProjectMember.objects.get( - pk=pk, - workspace__slug=slug, - project_id=project_id, - is_active=True, - ) - if request.user.id == project_member.member_id: - return Response( - {"error": "You cannot update your own role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - # Check while updating user roles - requested_project_member = ProjectMember.objects.get( - project_id=project_id, - workspace__slug=slug, - member=request.user, - is_active=True, - ) - if ( - "role" in request.data - and int(request.data.get("role", project_member.role)) - > requested_project_member.role - ): - return Response( - { - "error": "You cannot update a role that is higher than your own role" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = ProjectMemberSerializer( - project_member, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, pk): - project_member = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - pk=pk, - member__is_bot=False, - is_active=True, - ) - # check requesting user role - requesting_project_member = ProjectMember.objects.get( - workspace__slug=slug, - member=request.user, - project_id=project_id, - is_active=True, - ) - # User cannot remove himself - if str(project_member.id) == str(requesting_project_member.id): - return Response( - { - "error": "You cannot remove yourself from the workspace. Please use leave workspace" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - # User cannot deactivate higher role - if requesting_project_member.role < project_member.role: - return Response( - { - "error": "You cannot remove a user having role higher than you" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - project_member.is_active = False - project_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - def leave(self, request, slug, project_id): - project_member = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - member=request.user, - is_active=True, - ) - - # Check if the leaving user is the only admin of the project - if ( - project_member.role == 20 - and not ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - role=20, - is_active=True, - ).count() - > 1 - ): - return Response( - { - "error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - # Deactivate the user - project_member.is_active = False - project_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class AddTeamToProjectEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - - def post(self, request, slug, project_id): - team_members = TeamMember.objects.filter( - workspace__slug=slug, team__in=request.data.get("teams", []) - ).values_list("member", flat=True) - - if len(team_members) == 0: - return Response( - {"error": "No such team exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - project_members = [] - issue_props = [] - for member in team_members: - project_members.append( - ProjectMember( - project_id=project_id, - member_id=member, - workspace=workspace, - created_by=request.user, - ) - ) - issue_props.append( - IssueProperty( - project_id=project_id, - user_id=member, - workspace=workspace, - created_by=request.user, - ) - ) - - ProjectMember.objects.bulk_create( - project_members, batch_size=10, ignore_conflicts=True - ) - - _ = IssueProperty.objects.bulk_create( - issue_props, batch_size=10, ignore_conflicts=True - ) - - serializer = ProjectMemberSerializer(project_members, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - -class ProjectIdentifierEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] - - def get(self, request, slug): - name = request.GET.get("name", "").strip().upper() - - if name == "": - return Response( - {"error": "Name is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - exists = ProjectIdentifier.objects.filter( - name=name, workspace__slug=slug - ).values("id", "name", "project") - - return Response( - {"exists": len(exists), "identifiers": exists}, - status=status.HTTP_200_OK, - ) - - def delete(self, request, slug): - name = request.data.get("name", "").strip().upper() - - if name == "": - return Response( - {"error": "Name is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if Project.objects.filter( - identifier=name, workspace__slug=slug - ).exists(): - return Response( - { - "error": "Cannot delete an identifier of an existing project" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - ProjectIdentifier.objects.filter( - name=name, workspace__slug=slug - ).delete() - - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - - -class ProjectUserViewsEndpoint(BaseAPIView): - def post(self, request, slug, project_id): - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - project_member = ProjectMember.objects.filter( - member=request.user, - project=project, - is_active=True, - ).first() - - if project_member is None: - return Response( - {"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN - ) - - view_props = project_member.view_props - default_props = project_member.default_props - preferences = project_member.preferences - sort_order = project_member.sort_order - - project_member.view_props = request.data.get("view_props", view_props) - project_member.default_props = request.data.get( - "default_props", default_props - ) - project_member.preferences = request.data.get( - "preferences", preferences - ) - project_member.sort_order = request.data.get("sort_order", sort_order) - - project_member.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class ProjectMemberUserEndpoint(BaseAPIView): - def get(self, request, slug, project_id): - project_member = ProjectMember.objects.get( - project_id=project_id, - workspace__slug=slug, - member=request.user, - is_active=True, - ) - serializer = ProjectMemberSerializer(project_member) - - return Response(serializer.data, status=status.HTTP_200_OK) - - -class ProjectFavoritesViewSet(BaseViewSet): - serializer_class = ProjectFavoriteSerializer - model = ProjectFavorite - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(user=self.request.user) - .select_related( - "project", "project__project_lead", "project__default_assignee" - ) - .select_related("workspace", "workspace__owner") - ) - - def perform_create(self, serializer): - serializer.save(user=self.request.user) - - def create(self, request, slug): - serializer = ProjectFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id): - project_favorite = ProjectFavorite.objects.get( - project=project_id, user=request.user, workspace__slug=slug - ) - project_favorite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class ProjectPublicCoverImagesEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def get(self, request): - files = [] - s3 = boto3.client( - "s3", - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - ) - params = { - "Bucket": settings.AWS_STORAGE_BUCKET_NAME, - "Prefix": "static/project-cover/", - } - - response = s3.list_objects_v2(**params) - # Extracting file keys from the response - if "Contents" in response: - for content in response["Contents"]: - if not content["Key"].endswith( - "/" - ): # This line ensures we're only getting files, not "sub-folders" - files.append( - f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" - ) - - return Response(files, status=status.HTTP_200_OK) - - -class ProjectDeployBoardViewSet(BaseViewSet): - permission_classes = [ - ProjectMemberPermission, - ] - serializer_class = ProjectDeployBoardSerializer - model = ProjectDeployBoard - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - .select_related("project") - ) - - def create(self, request, slug, project_id): - comments = request.data.get("comments", False) - reactions = request.data.get("reactions", False) - inbox = request.data.get("inbox", None) - votes = request.data.get("votes", False) - views = request.data.get( - "views", - { - "list": True, - "kanban": True, - "calendar": True, - "gantt": True, - "spreadsheet": True, - }, - ) - - project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( - anchor=f"{slug}/{project_id}", - project_id=project_id, - ) - project_deploy_board.comments = comments - project_deploy_board.reactions = reactions - project_deploy_board.inbox = inbox - project_deploy_board.votes = votes - project_deploy_board.views = views - - project_deploy_board.save() - - serializer = ProjectDeployBoardSerializer(project_deploy_board) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class UserProjectRolesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceUserPermission, - ] - - def get(self, request, slug): - project_members = ProjectMember.objects.filter( - workspace__slug=slug, - member_id=request.user.id, - ).values("project_id", "role") - - project_members = { - str(member["project_id"]): member["role"] - for member in project_members - } - return Response(project_members, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py new file mode 100644 index 000000000..6deeea144 --- /dev/null +++ b/apiserver/plane/app/views/project/base.py @@ -0,0 +1,549 @@ +# Python imports +import boto3 + +# Django imports +from django.db import IntegrityError +from django.db.models import ( + Prefetch, + Q, + Exists, + OuterRef, + F, + Func, + Subquery, +) +from django.conf import settings + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework import serializers +from rest_framework.permissions import AllowAny + +# Module imports +from plane.app.views.base import BaseViewSet, BaseAPIView, WebhookMixin +from plane.app.serializers import ( + ProjectSerializer, + ProjectListSerializer, + ProjectFavoriteSerializer, + ProjectDeployBoardSerializer, +) + +from plane.app.permissions import ( + ProjectBasePermission, + ProjectMemberPermission, +) + +from plane.db.models import ( + Project, + ProjectMember, + Workspace, + State, + ProjectFavorite, + ProjectIdentifier, + Module, + Cycle, + Inbox, + ProjectDeployBoard, + IssueProperty, +) +from plane.utils.cache import cache_response + +class ProjectViewSet(WebhookMixin, BaseViewSet): + serializer_class = ProjectListSerializer + model = Project + webhook_event = "project" + + permission_classes = [ + ProjectBasePermission, + ] + + def get_queryset(self): + sort_order = ProjectMember.objects.filter( + member=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ).values("sort_order") + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter( + Q(project_projectmember__member=self.request.user) + | Q(network=2) + ) + .select_related( + "workspace", + "workspace__owner", + "default_assignee", + "project_lead", + ) + .annotate( + is_favorite=Exists( + ProjectFavorite.objects.filter( + user=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + ) + ) + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + member=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ) + ) + ) + .annotate( + total_members=ProjectMember.objects.filter( + project_id=OuterRef("id"), + member__is_bot=False, + is_active=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + total_cycles=Cycle.objects.filter(project_id=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + total_modules=Module.objects.filter(project_id=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + member_role=ProjectMember.objects.filter( + project_id=OuterRef("pk"), + member_id=self.request.user.id, + is_active=True, + ).values("role") + ) + .annotate( + is_deployed=Exists( + ProjectDeployBoard.objects.filter( + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + ) + ) + .annotate(sort_order=Subquery(sort_order)) + .prefetch_related( + Prefetch( + "project_projectmember", + queryset=ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ).select_related("member"), + to_attr="members_list", + ) + ) + .distinct() + ) + + def list(self, request, slug): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + projects = self.get_queryset().order_by("sort_order", "name") + if request.GET.get("per_page", False) and request.GET.get( + "cursor", False + ): + return self.paginate( + request=request, + queryset=(projects), + on_results=lambda projects: ProjectListSerializer( + projects, many=True + ).data, + ) + projects = ProjectListSerializer( + projects, many=True, fields=fields if fields else None + ).data + return Response(projects, status=status.HTTP_200_OK) + + def create(self, request, slug): + try: + workspace = Workspace.objects.get(slug=slug) + + serializer = ProjectSerializer( + data={**request.data}, context={"workspace_id": workspace.id} + ) + if serializer.is_valid(): + serializer.save() + + # Add the user as Administrator to the project + _ = ProjectMember.objects.create( + project_id=serializer.data["id"], + member=request.user, + role=20, + ) + # Also create the issue property for the user + _ = IssueProperty.objects.create( + project_id=serializer.data["id"], + user=request.user, + ) + + if serializer.data["project_lead"] is not None and str( + serializer.data["project_lead"] + ) != str(request.user.id): + ProjectMember.objects.create( + project_id=serializer.data["id"], + member_id=serializer.data["project_lead"], + role=20, + ) + # Also create the issue property for the user + IssueProperty.objects.create( + project_id=serializer.data["id"], + user_id=serializer.data["project_lead"], + ) + + # Default states + states = [ + { + "name": "Backlog", + "color": "#A3A3A3", + "sequence": 15000, + "group": "backlog", + "default": True, + }, + { + "name": "Todo", + "color": "#3A3A3A", + "sequence": 25000, + "group": "unstarted", + }, + { + "name": "In Progress", + "color": "#F59E0B", + "sequence": 35000, + "group": "started", + }, + { + "name": "Done", + "color": "#16A34A", + "sequence": 45000, + "group": "completed", + }, + { + "name": "Cancelled", + "color": "#EF4444", + "sequence": 55000, + "group": "cancelled", + }, + ] + + State.objects.bulk_create( + [ + State( + name=state["name"], + color=state["color"], + project=serializer.instance, + sequence=state["sequence"], + workspace=serializer.instance.workspace, + group=state["group"], + default=state.get("default", False), + created_by=request.user, + ) + for state in states + ] + ) + + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) + serializer = ProjectListSerializer(project) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST, + ) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"name": "The project name is already taken"}, + status=status.HTTP_410_GONE, + ) + except Workspace.DoesNotExist as e: + return Response( + {"error": "Workspace does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + except serializers.ValidationError as e: + return Response( + {"identifier": "The project identifier is already taken"}, + status=status.HTTP_410_GONE, + ) + + def partial_update(self, request, slug, pk=None): + try: + workspace = Workspace.objects.get(slug=slug) + + project = Project.objects.get(pk=pk) + + serializer = ProjectSerializer( + project, + data={**request.data}, + context={"workspace_id": workspace.id}, + partial=True, + ) + + if serializer.is_valid(): + serializer.save() + if serializer.data["inbox_view"]: + Inbox.objects.get_or_create( + name=f"{project.name} Inbox", + project=project, + is_default=True, + ) + + # Create the triage state in Backlog group + State.objects.get_or_create( + name="Triage", + group="backlog", + description="Default state for managing all Inbox Issues", + project_id=pk, + color="#ff7700", + ) + + project = ( + self.get_queryset() + .filter(pk=serializer.data["id"]) + .first() + ) + serializer = ProjectListSerializer(project) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"name": "The project name is already taken"}, + status=status.HTTP_410_GONE, + ) + except (Project.DoesNotExist, Workspace.DoesNotExist): + return Response( + {"error": "Project does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + except serializers.ValidationError as e: + return Response( + {"identifier": "The project identifier is already taken"}, + status=status.HTTP_410_GONE, + ) + + +class ProjectIdentifierEndpoint(BaseAPIView): + permission_classes = [ + ProjectBasePermission, + ] + + def get(self, request, slug): + name = request.GET.get("name", "").strip().upper() + + if name == "": + return Response( + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + exists = ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug + ).values("id", "name", "project") + + return Response( + {"exists": len(exists), "identifiers": exists}, + status=status.HTTP_200_OK, + ) + + def delete(self, request, slug): + name = request.data.get("name", "").strip().upper() + + if name == "": + return Response( + {"error": "Name is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if Project.objects.filter( + identifier=name, workspace__slug=slug + ).exists(): + return Response( + { + "error": "Cannot delete an identifier of an existing project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug + ).delete() + + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + + +class ProjectUserViewsEndpoint(BaseAPIView): + def post(self, request, slug, project_id): + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + project_member = ProjectMember.objects.filter( + member=request.user, + project=project, + is_active=True, + ).first() + + if project_member is None: + return Response( + {"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN + ) + + view_props = project_member.view_props + default_props = project_member.default_props + preferences = project_member.preferences + sort_order = project_member.sort_order + + project_member.view_props = request.data.get("view_props", view_props) + project_member.default_props = request.data.get( + "default_props", default_props + ) + project_member.preferences = request.data.get( + "preferences", preferences + ) + project_member.sort_order = request.data.get("sort_order", sort_order) + + project_member.save() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProjectFavoritesViewSet(BaseViewSet): + serializer_class = ProjectFavoriteSerializer + model = ProjectFavorite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(user=self.request.user) + .select_related( + "project", "project__project_lead", "project__default_assignee" + ) + .select_related("workspace", "workspace__owner") + ) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def create(self, request, slug): + serializer = ProjectFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id): + project_favorite = ProjectFavorite.objects.get( + project=project_id, user=request.user, workspace__slug=slug + ) + project_favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProjectPublicCoverImagesEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + # Cache the below api for 24 hours + @cache_response(60 * 60 * 24, user=False) + def get(self, request): + files = [] + s3 = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + params = { + "Bucket": settings.AWS_STORAGE_BUCKET_NAME, + "Prefix": "static/project-cover/", + } + + response = s3.list_objects_v2(**params) + # Extracting file keys from the response + if "Contents" in response: + for content in response["Contents"]: + if not content["Key"].endswith( + "/" + ): # This line ensures we're only getting files, not "sub-folders" + files.append( + f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" + ) + + return Response(files, status=status.HTTP_200_OK) + + +class ProjectDeployBoardViewSet(BaseViewSet): + permission_classes = [ + ProjectMemberPermission, + ] + serializer_class = ProjectDeployBoardSerializer + model = ProjectDeployBoard + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + .select_related("project") + ) + + def create(self, request, slug, project_id): + comments = request.data.get("comments", False) + reactions = request.data.get("reactions", False) + inbox = request.data.get("inbox", None) + votes = request.data.get("votes", False) + views = request.data.get( + "views", + { + "list": True, + "kanban": True, + "calendar": True, + "gantt": True, + "spreadsheet": True, + }, + ) + + project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( + anchor=f"{slug}/{project_id}", + project_id=project_id, + ) + project_deploy_board.comments = comments + project_deploy_board.reactions = reactions + project_deploy_board.inbox = inbox + project_deploy_board.votes = votes + project_deploy_board.views = views + + project_deploy_board.save() + + serializer = ProjectDeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/invite.py b/apiserver/plane/app/views/project/invite.py new file mode 100644 index 000000000..d199a8770 --- /dev/null +++ b/apiserver/plane/app/views/project/invite.py @@ -0,0 +1,286 @@ +# Python imports +import jwt +from datetime import datetime + +# Django imports +from django.core.exceptions import ValidationError +from django.core.validators import validate_email +from django.conf import settings +from django.utils import timezone + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import AllowAny + +# Module imports +from .base import BaseViewSet, BaseAPIView +from plane.app.serializers import ProjectMemberInviteSerializer + +from plane.app.permissions import ProjectBasePermission + +from plane.db.models import ( + ProjectMember, + Workspace, + ProjectMemberInvite, + User, + WorkspaceMember, + IssueProperty, +) + + +class ProjectInvitationsViewset(BaseViewSet): + serializer_class = ProjectMemberInviteSerializer + model = ProjectMemberInvite + + search_fields = [] + + permission_classes = [ + ProjectBasePermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .select_related("project") + .select_related("workspace", "workspace__owner") + ) + + def create(self, request, slug, project_id): + emails = request.data.get("emails", []) + + # Check if email is provided + if not emails: + return Response( + {"error": "Emails are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + requesting_user = ProjectMember.objects.get( + workspace__slug=slug, + project_id=project_id, + member_id=request.user.id, + ) + + # Check if any invited user has an higher role + if len( + [ + email + for email in emails + if int(email.get("role", 10)) > requesting_user.role + ] + ): + return Response( + {"error": "You cannot invite a user with higher role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + + project_invitations = [] + for email in emails: + try: + validate_email(email.get("email")) + project_invitations.append( + ProjectMemberInvite( + email=email.get("email").strip().lower(), + project_id=project_id, + workspace_id=workspace.id, + token=jwt.encode( + { + "email": email, + "timestamp": datetime.now().timestamp(), + }, + settings.SECRET_KEY, + algorithm="HS256", + ), + role=email.get("role", 10), + created_by=request.user, + ) + ) + except ValidationError: + return Response( + { + "error": f"Invalid email - {email} provided a valid email address is required to send the invite" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Create workspace member invite + project_invitations = ProjectMemberInvite.objects.bulk_create( + project_invitations, batch_size=10, ignore_conflicts=True + ) + current_site = request.META.get("HTTP_ORIGIN") + + # Send invitations + for invitation in project_invitations: + project_invitations.delay( + invitation.email, + project_id, + invitation.token, + current_site, + request.user.email, + ) + + return Response( + { + "message": "Email sent successfully", + }, + status=status.HTTP_200_OK, + ) + + +class UserProjectInvitationsViewset(BaseViewSet): + serializer_class = ProjectMemberInviteSerializer + model = ProjectMemberInvite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(email=self.request.user.email) + .select_related("workspace", "workspace__owner", "project") + ) + + def create(self, request, slug): + project_ids = request.data.get("project_ids", []) + + # Get the workspace user role + workspace_member = WorkspaceMember.objects.get( + member=request.user, + workspace__slug=slug, + is_active=True, + ) + + workspace_role = workspace_member.role + workspace = workspace_member.workspace + + # If the user was already part of workspace + _ = ProjectMember.objects.filter( + workspace__slug=slug, + project_id__in=project_ids, + member=request.user, + ).update(is_active=True) + + ProjectMember.objects.bulk_create( + [ + ProjectMember( + project_id=project_id, + member=request.user, + role=15 if workspace_role >= 15 else 10, + workspace=workspace, + created_by=request.user, + ) + for project_id in project_ids + ], + ignore_conflicts=True, + ) + + IssueProperty.objects.bulk_create( + [ + IssueProperty( + project_id=project_id, + user=request.user, + workspace=workspace, + created_by=request.user, + ) + for project_id in project_ids + ], + ignore_conflicts=True, + ) + + return Response( + {"message": "Projects joined successfully"}, + status=status.HTTP_201_CREATED, + ) + + +class ProjectJoinEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def post(self, request, slug, project_id, pk): + project_invite = ProjectMemberInvite.objects.get( + pk=pk, + project_id=project_id, + workspace__slug=slug, + ) + + email = request.data.get("email", "") + + if email == "" or project_invite.email != email: + return Response( + {"error": "You do not have permission to join the project"}, + status=status.HTTP_403_FORBIDDEN, + ) + + if project_invite.responded_at is None: + project_invite.accepted = request.data.get("accepted", False) + project_invite.responded_at = timezone.now() + project_invite.save() + + if project_invite.accepted: + # Check if the user account exists + user = User.objects.filter(email=email).first() + + # Check if user is a part of workspace + workspace_member = WorkspaceMember.objects.filter( + workspace__slug=slug, member=user + ).first() + # Add him to workspace + if workspace_member is None: + _ = WorkspaceMember.objects.create( + workspace_id=project_invite.workspace_id, + member=user, + role=( + 15 + if project_invite.role >= 15 + else project_invite.role + ), + ) + else: + # Else make him active + workspace_member.is_active = True + workspace_member.save() + + # Check if the user was already a member of project then activate the user + project_member = ProjectMember.objects.filter( + workspace_id=project_invite.workspace_id, member=user + ).first() + if project_member is None: + # Create a Project Member + _ = ProjectMember.objects.create( + workspace_id=project_invite.workspace_id, + member=user, + role=project_invite.role, + ) + else: + project_member.is_active = True + project_member.role = project_member.role + project_member.save() + + return Response( + {"message": "Project Invitation Accepted"}, + status=status.HTTP_200_OK, + ) + + return Response( + {"message": "Project Invitation was not accepted"}, + status=status.HTTP_200_OK, + ) + + return Response( + {"error": "You have already responded to the invitation request"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get(self, request, slug, project_id, pk): + project_invitation = ProjectMemberInvite.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + serializer = ProjectMemberInviteSerializer(project_invitation) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py new file mode 100644 index 000000000..187dfc8d0 --- /dev/null +++ b/apiserver/plane/app/views/project/member.py @@ -0,0 +1,349 @@ +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .base import BaseViewSet, BaseAPIView +from plane.app.serializers import ( + ProjectMemberSerializer, + ProjectMemberAdminSerializer, + ProjectMemberRoleSerializer, +) + +from plane.app.permissions import ( + ProjectBasePermission, + ProjectMemberPermission, + ProjectLitePermission, + WorkspaceUserPermission, +) + +from plane.db.models import ( + Project, + ProjectMember, + Workspace, + TeamMember, + IssueProperty, +) + + +class ProjectMemberViewSet(BaseViewSet): + serializer_class = ProjectMemberAdminSerializer + model = ProjectMember + permission_classes = [ + ProjectMemberPermission, + ] + + def get_permissions(self): + if self.action == "leave": + self.permission_classes = [ + ProjectLitePermission, + ] + else: + self.permission_classes = [ + ProjectMemberPermission, + ] + + return super(ProjectMemberViewSet, self).get_permissions() + + search_fields = [ + "member__display_name", + "member__first_name", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(member__is_bot=False) + .filter() + .select_related("project") + .select_related("member") + .select_related("workspace", "workspace__owner") + ) + + def create(self, request, slug, project_id): + members = request.data.get("members", []) + + # get the project + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + if not len(members): + return Response( + {"error": "Atleast one member is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + bulk_project_members = [] + bulk_issue_props = [] + + project_members = ( + ProjectMember.objects.filter( + workspace__slug=slug, + member_id__in=[member.get("member_id") for member in members], + ) + .values("member_id", "sort_order") + .order_by("sort_order") + ) + + bulk_project_members = [] + member_roles = { + member.get("member_id"): member.get("role") for member in members + } + # Update roles in the members array based on the member_roles dictionary + for project_member in ProjectMember.objects.filter( + project_id=project_id, + member_id__in=[member.get("member_id") for member in members], + ): + project_member.role = member_roles[str(project_member.member_id)] + project_member.is_active = True + bulk_project_members.append(project_member) + + # Update the roles of the existing members + ProjectMember.objects.bulk_update( + bulk_project_members, ["is_active", "role"], batch_size=100 + ) + + for member in members: + sort_order = [ + project_member.get("sort_order") + for project_member in project_members + if str(project_member.get("member_id")) + == str(member.get("member_id")) + ] + bulk_project_members.append( + ProjectMember( + member_id=member.get("member_id"), + role=member.get("role", 10), + project_id=project_id, + workspace_id=project.workspace_id, + sort_order=( + sort_order[0] - 10000 if len(sort_order) else 65535 + ), + ) + ) + bulk_issue_props.append( + IssueProperty( + user_id=member.get("member_id"), + project_id=project_id, + workspace_id=project.workspace_id, + ) + ) + + project_members = ProjectMember.objects.bulk_create( + bulk_project_members, + batch_size=10, + ignore_conflicts=True, + ) + + _ = IssueProperty.objects.bulk_create( + bulk_issue_props, batch_size=10, ignore_conflicts=True + ) + + project_members = ProjectMember.objects.filter( + project_id=project_id, + member_id__in=[member.get("member_id") for member in members], + ) + serializer = ProjectMemberRoleSerializer(project_members, many=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def list(self, request, slug, project_id): + # Get the list of project members for the project + project_members = ProjectMember.objects.filter( + project_id=project_id, + workspace__slug=slug, + member__is_bot=False, + is_active=True, + ).select_related("project", "member", "workspace") + + serializer = ProjectMemberRoleSerializer( + project_members, fields=("id", "member", "role"), many=True + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, slug, project_id, pk): + project_member = ProjectMember.objects.get( + pk=pk, + workspace__slug=slug, + project_id=project_id, + is_active=True, + ) + if request.user.id == project_member.member_id: + return Response( + {"error": "You cannot update your own role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + # Check while updating user roles + requested_project_member = ProjectMember.objects.get( + project_id=project_id, + workspace__slug=slug, + member=request.user, + is_active=True, + ) + if ( + "role" in request.data + and int(request.data.get("role", project_member.role)) + > requested_project_member.role + ): + return Response( + { + "error": "You cannot update a role that is higher than your own role" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = ProjectMemberSerializer( + project_member, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, pk): + project_member = ProjectMember.objects.get( + workspace__slug=slug, + project_id=project_id, + pk=pk, + member__is_bot=False, + is_active=True, + ) + # check requesting user role + requesting_project_member = ProjectMember.objects.get( + workspace__slug=slug, + member=request.user, + project_id=project_id, + is_active=True, + ) + # User cannot remove himself + if str(project_member.id) == str(requesting_project_member.id): + return Response( + { + "error": "You cannot remove yourself from the workspace. Please use leave workspace" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # User cannot deactivate higher role + if requesting_project_member.role < project_member.role: + return Response( + { + "error": "You cannot remove a user having role higher than you" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + project_member.is_active = False + project_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + def leave(self, request, slug, project_id): + project_member = ProjectMember.objects.get( + workspace__slug=slug, + project_id=project_id, + member=request.user, + is_active=True, + ) + + # Check if the leaving user is the only admin of the project + if ( + project_member.role == 20 + and not ProjectMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + role=20, + is_active=True, + ).count() + > 1 + ): + return Response( + { + "error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin", + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Deactivate the user + project_member.is_active = False + project_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class AddTeamToProjectEndpoint(BaseAPIView): + permission_classes = [ + ProjectBasePermission, + ] + + def post(self, request, slug, project_id): + team_members = TeamMember.objects.filter( + workspace__slug=slug, team__in=request.data.get("teams", []) + ).values_list("member", flat=True) + + if len(team_members) == 0: + return Response( + {"error": "No such team exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + + project_members = [] + issue_props = [] + for member in team_members: + project_members.append( + ProjectMember( + project_id=project_id, + member_id=member, + workspace=workspace, + created_by=request.user, + ) + ) + issue_props.append( + IssueProperty( + project_id=project_id, + user_id=member, + workspace=workspace, + created_by=request.user, + ) + ) + + ProjectMember.objects.bulk_create( + project_members, batch_size=10, ignore_conflicts=True + ) + + _ = IssueProperty.objects.bulk_create( + issue_props, batch_size=10, ignore_conflicts=True + ) + + serializer = ProjectMemberSerializer(project_members, many=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +class ProjectMemberUserEndpoint(BaseAPIView): + def get(self, request, slug, project_id): + project_member = ProjectMember.objects.get( + project_id=project_id, + workspace__slug=slug, + member=request.user, + is_active=True, + ) + serializer = ProjectMemberSerializer(project_member) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class UserProjectRolesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceUserPermission, + ] + + def get(self, request, slug): + project_members = ProjectMember.objects.filter( + workspace__slug=slug, + member_id=request.user.id, + ).values("project_id", "role") + + project_members = { + str(member["project_id"]): member["role"] + for member in project_members + } + return Response(project_members, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/search.py b/apiserver/plane/app/views/search.py index a2ed1c015..42aa05e4f 100644 --- a/apiserver/plane/app/views/search.py +++ b/apiserver/plane/app/views/search.py @@ -235,6 +235,7 @@ class IssueSearchEndpoint(BaseAPIView): cycle = request.query_params.get("cycle", "false") module = request.query_params.get("module", False) sub_issue = request.query_params.get("sub_issue", "false") + target_date = request.query_params.get("target_date", True) issue_id = request.query_params.get("issue_id", False) @@ -253,7 +254,8 @@ class IssueSearchEndpoint(BaseAPIView): if parent == "true" and issue_id: issue = Issue.issue_objects.get(pk=issue_id) issues = issues.filter( - ~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id)) + ~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id) + ) if issue_relation == "true" and issue_id: issue = Issue.issue_objects.get(pk=issue_id) issues = issues.filter( @@ -273,6 +275,9 @@ class IssueSearchEndpoint(BaseAPIView): if module: issues = issues.exclude(issue_module__module=module) + if target_date == "none": + issues = issues.filter(target_date__isnull=True) + return Response( issues.values( "name", diff --git a/apiserver/plane/app/views/state.py b/apiserver/plane/app/views/state/base.py similarity index 91% rename from apiserver/plane/app/views/state.py rename to apiserver/plane/app/views/state/base.py index 34b3d1dcc..137a89d99 100644 --- a/apiserver/plane/app/views/state.py +++ b/apiserver/plane/app/views/state/base.py @@ -9,14 +9,13 @@ from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseViewSet, BaseAPIView +from .. import BaseViewSet from plane.app.serializers import StateSerializer from plane.app.permissions import ( ProjectEntityPermission, - WorkspaceEntityPermission, ) from plane.db.models import State, Issue - +from plane.utils.cache import invalidate_cache class StateViewSet(BaseViewSet): serializer_class = StateSerializer @@ -41,6 +40,7 @@ class StateViewSet(BaseViewSet): .distinct() ) + @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) def create(self, request, slug, project_id): serializer = StateSerializer(data=request.data) if serializer.is_valid(): @@ -61,6 +61,7 @@ class StateViewSet(BaseViewSet): return Response(state_dict, status=status.HTTP_200_OK) return Response(states, status=status.HTTP_200_OK) + @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) def mark_as_default(self, request, slug, project_id, pk): # Select all the states which are marked as default _ = State.objects.filter( @@ -71,6 +72,7 @@ class StateViewSet(BaseViewSet): ).update(default=True) return Response(status=status.HTTP_204_NO_CONTENT) + @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) def destroy(self, request, slug, project_id, pk): state = State.objects.get( ~Q(name="Triage"), diff --git a/apiserver/plane/app/views/user.py b/apiserver/plane/app/views/user/base.py similarity index 89% rename from apiserver/plane/app/views/user.py rename to apiserver/plane/app/views/user/base.py index 7764e3b97..4d69d1cf2 100644 --- a/apiserver/plane/app/views/user.py +++ b/apiserver/plane/app/views/user/base.py @@ -1,25 +1,24 @@ -# Third party imports -from rest_framework.response import Response -from rest_framework import status +# Django imports +from django.db.models import Case, Count, IntegerField, Q, When +# Third party imports +from rest_framework import status +from rest_framework.response import Response # Module imports from plane.app.serializers import ( - UserSerializer, IssueActivitySerializer, UserMeSerializer, UserMeSettingsSerializer, + UserSerializer, ) - -from plane.app.views.base import BaseViewSet, BaseAPIView -from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember +from plane.app.views.base import BaseAPIView, BaseViewSet +from plane.db.models import IssueActivity, ProjectMember, User, WorkspaceMember from plane.license.models import Instance, InstanceAdmin +from plane.utils.cache import cache_response, invalidate_cache from plane.utils.paginator import BasePaginator -from django.db.models import Q, F, Count, Case, When, IntegerField - - class UserEndpoint(BaseViewSet): serializer_class = UserSerializer model = User @@ -27,6 +26,7 @@ class UserEndpoint(BaseViewSet): def get_object(self): return self.request.user + @cache_response(60 * 60) def retrieve(self, request): serialized_data = UserMeSerializer(request.user).data return Response( @@ -34,10 +34,12 @@ class UserEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) + @cache_response(60 * 60) def retrieve_user_settings(self, request): serialized_data = UserMeSettingsSerializer(request.user).data return Response(serialized_data, status=status.HTTP_200_OK) + @cache_response(60 * 60) def retrieve_instance_admin(self, request): instance = Instance.objects.first() is_admin = InstanceAdmin.objects.filter( @@ -47,6 +49,11 @@ class UserEndpoint(BaseViewSet): {"is_instance_admin": is_admin}, status=status.HTTP_200_OK ) + @invalidate_cache(path="/api/users/me/") + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/users/me/") def deactivate(self, request): # Check all workspace user is active user = self.get_object() @@ -145,6 +152,8 @@ class UserEndpoint(BaseViewSet): class UpdateUserOnBoardedEndpoint(BaseAPIView): + + @invalidate_cache(path="/api/users/me/") def patch(self, request): user = User.objects.get(pk=request.user.id, is_active=True) user.is_onboarded = request.data.get("is_onboarded", False) @@ -155,6 +164,8 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView): class UpdateUserTourCompletedEndpoint(BaseAPIView): + + @invalidate_cache(path="/api/users/me/") def patch(self, request): user = User.objects.get(pk=request.user.id, is_active=True) user.is_tour_completed = request.data.get("is_tour_completed", False) @@ -165,6 +176,7 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView): class UserActivityEndpoint(BaseAPIView, BasePaginator): + def get(self, request): queryset = IssueActivity.objects.filter( actor=request.user diff --git a/apiserver/plane/app/views/view.py b/apiserver/plane/app/views/view/base.py similarity index 97% rename from apiserver/plane/app/views/view.py rename to apiserver/plane/app/views/view/base.py index 769aa1adb..14e9f9e4e 100644 --- a/apiserver/plane/app/views/view.py +++ b/apiserver/plane/app/views/view/base.py @@ -1,58 +1,56 @@ # Django imports +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField from django.db.models import ( - Q, - OuterRef, - Func, - F, Case, - Value, CharField, - When, Exists, + F, + Func, Max, + OuterRef, + Q, + UUIDField, + Value, + When, ) +from django.db.models.functions import Coalesce from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import Value, UUIDField -from django.db.models.functions import Coalesce -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import Value, UUIDField +from rest_framework import status # Third party imports from rest_framework.response import Response -from rest_framework import status -# Module imports -from . import BaseViewSet +from plane.app.permissions import ( + ProjectEntityPermission, + WorkspaceEntityPermission, +) from plane.app.serializers import ( - IssueViewSerializer, IssueSerializer, IssueViewFavoriteSerializer, -) -from plane.app.permissions import ( - WorkspaceEntityPermission, - ProjectEntityPermission, + IssueViewSerializer, ) from plane.db.models import ( - Workspace, - IssueView, Issue, - IssueViewFavorite, - IssueLink, IssueAttachment, + IssueLink, + IssueView, + IssueViewFavorite, + Workspace, ) from plane.utils.grouper import ( - issue_queryset_grouper, - issue_on_results, issue_group_values, + issue_on_results, + issue_queryset_grouper, ) from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset from plane.utils.paginator import GroupedOffsetPaginator +# Module imports +from .. import BaseViewSet + class GlobalViewViewSet(BaseViewSet): serializer_class = IssueViewSerializer diff --git a/apiserver/plane/app/views/webhook.py b/apiserver/plane/app/views/webhook/base.py similarity index 98% rename from apiserver/plane/app/views/webhook.py rename to apiserver/plane/app/views/webhook/base.py index fe69cd7e6..9586722a0 100644 --- a/apiserver/plane/app/views/webhook.py +++ b/apiserver/plane/app/views/webhook/base.py @@ -8,7 +8,7 @@ from rest_framework.response import Response # Module imports from plane.db.models import Webhook, WebhookLog, Workspace from plane.db.models.webhook import generate_token -from .base import BaseAPIView +from ..base import BaseAPIView from plane.app.permissions import WorkspaceOwnerPermission from plane.app.serializers import WebhookSerializer, WebhookLogSerializer @@ -41,7 +41,7 @@ class WebhookEndpoint(BaseAPIView): raise IntegrityError def get(self, request, slug, pk=None): - if pk == None: + if pk is None: webhooks = Webhook.objects.filter(workspace__slug=slug) serializer = WebhookSerializer( webhooks, diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py deleted file mode 100644 index b7fb9a676..000000000 --- a/apiserver/plane/app/views/workspace.py +++ /dev/null @@ -1,1693 +0,0 @@ -# Python imports -import jwt -from datetime import date, datetime -from dateutil.relativedelta import relativedelta - -# Django imports -from django.db import IntegrityError -from django.conf import settings -from django.utils import timezone -from django.core.exceptions import ValidationError -from django.core.validators import validate_email -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Count, - Case, - Value, - CharField, - When, - Max, - IntegerField, - Sum, -) -from django.db.models.functions import ExtractWeek, Cast, ExtractDay -from django.db.models.fields import DateField -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import Value, UUIDField -from django.db.models.functions import Coalesce - -# Third party modules -from rest_framework import status -from rest_framework.response import Response -from rest_framework.permissions import AllowAny - -# Module imports -from plane.app.serializers import ( - WorkSpaceSerializer, - WorkSpaceMemberSerializer, - TeamSerializer, - WorkSpaceMemberInviteSerializer, - UserLiteSerializer, - ProjectMemberSerializer, - WorkspaceThemeSerializer, - IssueActivitySerializer, - IssueSerializer, - WorkspaceMemberAdminSerializer, - WorkspaceMemberMeSerializer, - ProjectMemberRoleSerializer, - WorkspaceUserPropertiesSerializer, - WorkspaceEstimateSerializer, - StateSerializer, - LabelSerializer, -) -from plane.app.views.base import BaseAPIView -from . import BaseViewSet -from plane.db.models import ( - State, - User, - Workspace, - WorkspaceMemberInvite, - Team, - ProjectMember, - IssueActivity, - Issue, - WorkspaceTheme, - IssueLink, - IssueAttachment, - IssueSubscriber, - Project, - Label, - WorkspaceMember, - CycleIssue, - WorkspaceUserProperties, - Estimate, - EstimatePoint, - Module, - ModuleLink, - Cycle, -) -from plane.app.permissions import ( - WorkSpaceBasePermission, - WorkSpaceAdminPermission, - WorkspaceEntityPermission, - WorkspaceViewerPermission, - WorkspaceUserPermission, -) -from plane.bgtasks.workspace_invitation_task import workspace_invitation -from plane.utils.issue_filters import issue_filters -from plane.bgtasks.event_tracking_task import workspace_invite_event -from plane.app.serializers.module import ( - ModuleSerializer, -) -from plane.app.serializers.cycle import ( - CycleSerializer, -) - - -class WorkSpaceViewSet(BaseViewSet): - model = Workspace - serializer_class = WorkSpaceSerializer - permission_classes = [ - WorkSpaceBasePermission, - ] - - search_fields = [ - "name", - ] - filterset_fields = [ - "owner", - ] - - lookup_field = "slug" - - def get_queryset(self): - member_count = ( - WorkspaceMember.objects.filter( - workspace=OuterRef("id"), - member__is_bot=False, - is_active=True, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - - issue_count = ( - Issue.issue_objects.filter(workspace=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - return ( - self.filter_queryset( - super().get_queryset().select_related("owner") - ) - .order_by("name") - .filter( - workspace_member__member=self.request.user, - workspace_member__is_active=True, - ) - .annotate(total_members=member_count) - .annotate(total_issues=issue_count) - .select_related("owner") - ) - - def create(self, request): - try: - serializer = WorkSpaceSerializer(data=request.data) - - slug = request.data.get("slug", False) - name = request.data.get("name", False) - - if not name or not slug: - return Response( - {"error": "Both name and slug are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if len(name) > 80 or len(slug) > 48: - return Response( - { - "error": "The maximum length for name is 80 and for slug is 48" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if serializer.is_valid(): - serializer.save(owner=request.user) - # Create Workspace member - _ = WorkspaceMember.objects.create( - workspace_id=serializer.data["id"], - member=request.user, - role=20, - company_role=request.data.get("company_role", ""), - ) - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - [serializer.errors[error][0] for error in serializer.errors], - status=status.HTTP_400_BAD_REQUEST, - ) - - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"slug": "The workspace with the slug already exists"}, - status=status.HTTP_410_GONE, - ) - - -class UserWorkSpacesEndpoint(BaseAPIView): - search_fields = [ - "name", - ] - filterset_fields = [ - "owner", - ] - - def get(self, request): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - member_count = ( - WorkspaceMember.objects.filter( - workspace=OuterRef("id"), - member__is_bot=False, - is_active=True, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - - issue_count = ( - Issue.issue_objects.filter(workspace=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - - workspace = ( - Workspace.objects.prefetch_related( - Prefetch( - "workspace_member", - queryset=WorkspaceMember.objects.filter( - member=request.user, is_active=True - ), - ) - ) - .select_related("owner") - .annotate(total_members=member_count) - .annotate(total_issues=issue_count) - .filter( - workspace_member__member=request.user, - workspace_member__is_active=True, - ) - .distinct() - ) - workspaces = WorkSpaceSerializer( - self.filter_queryset(workspace), - fields=fields if fields else None, - many=True, - ).data - return Response(workspaces, status=status.HTTP_200_OK) - - -class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): - def get(self, request): - slug = request.GET.get("slug", False) - - if not slug or slug == "": - return Response( - {"error": "Workspace Slug is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.filter(slug=slug).exists() - return Response({"status": not workspace}, status=status.HTTP_200_OK) - - -class WorkspaceInvitationsViewset(BaseViewSet): - """Endpoint for creating, listing and deleting workspaces""" - - serializer_class = WorkSpaceMemberInviteSerializer - model = WorkspaceMemberInvite - - permission_classes = [ - WorkSpaceAdminPermission, - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "workspace__owner", "created_by") - ) - - def create(self, request, slug): - emails = request.data.get("emails", []) - # Check if email is provided - if not emails: - return Response( - {"error": "Emails are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # check for role level of the requesting user - requesting_user = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - - # Check if any invited user has an higher role - if len( - [ - email - for email in emails - if int(email.get("role", 10)) > requesting_user.role - ] - ): - return Response( - {"error": "You cannot invite a user with higher role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the workspace object - workspace = Workspace.objects.get(slug=slug) - - # Check if user is already a member of workspace - workspace_members = WorkspaceMember.objects.filter( - workspace_id=workspace.id, - member__email__in=[email.get("email") for email in emails], - is_active=True, - ).select_related("member", "workspace", "workspace__owner") - - if workspace_members: - return Response( - { - "error": "Some users are already member of workspace", - "workspace_users": WorkSpaceMemberSerializer( - workspace_members, many=True - ).data, - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace_invitations = [] - for email in emails: - try: - validate_email(email.get("email")) - workspace_invitations.append( - WorkspaceMemberInvite( - email=email.get("email").strip().lower(), - workspace_id=workspace.id, - token=jwt.encode( - { - "email": email, - "timestamp": datetime.now().timestamp(), - }, - settings.SECRET_KEY, - algorithm="HS256", - ), - role=email.get("role", 10), - created_by=request.user, - ) - ) - except ValidationError: - return Response( - { - "error": f"Invalid email - {email} provided a valid email address is required to send the invite" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - # Create workspace member invite - workspace_invitations = WorkspaceMemberInvite.objects.bulk_create( - workspace_invitations, batch_size=10, ignore_conflicts=True - ) - - current_site = request.META.get("HTTP_ORIGIN") - - # Send invitations - for invitation in workspace_invitations: - workspace_invitation.delay( - invitation.email, - workspace.id, - invitation.token, - current_site, - request.user.email, - ) - - return Response( - { - "message": "Emails sent successfully", - }, - status=status.HTTP_200_OK, - ) - - def destroy(self, request, slug, pk): - workspace_member_invite = WorkspaceMemberInvite.objects.get( - pk=pk, workspace__slug=slug - ) - workspace_member_invite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkspaceJoinEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - """Invitation response endpoint the user can respond to the invitation""" - - def post(self, request, slug, pk): - workspace_invite = WorkspaceMemberInvite.objects.get( - pk=pk, workspace__slug=slug - ) - - email = request.data.get("email", "") - - # Check the email - if email == "" or workspace_invite.email != email: - return Response( - {"error": "You do not have permission to join the workspace"}, - status=status.HTTP_403_FORBIDDEN, - ) - - # If already responded then return error - if workspace_invite.responded_at is None: - workspace_invite.accepted = request.data.get("accepted", False) - workspace_invite.responded_at = timezone.now() - workspace_invite.save() - - if workspace_invite.accepted: - # Check if the user created account after invitation - user = User.objects.filter(email=email).first() - - # If the user is present then create the workspace member - if user is not None: - # Check if the user was already a member of workspace then activate the user - workspace_member = WorkspaceMember.objects.filter( - workspace=workspace_invite.workspace, member=user - ).first() - if workspace_member is not None: - workspace_member.is_active = True - workspace_member.role = workspace_invite.role - workspace_member.save() - else: - # Create a Workspace - _ = WorkspaceMember.objects.create( - workspace=workspace_invite.workspace, - member=user, - role=workspace_invite.role, - ) - - # Set the user last_workspace_id to the accepted workspace - user.last_workspace_id = workspace_invite.workspace.id - user.save() - - # Delete the invitation - workspace_invite.delete() - - # Send event - workspace_invite_event.delay( - user=user.id if user is not None else None, - email=email, - user_agent=request.META.get("HTTP_USER_AGENT"), - ip=request.META.get("REMOTE_ADDR"), - event_name="MEMBER_ACCEPTED", - accepted_from="EMAIL", - ) - - return Response( - {"message": "Workspace Invitation Accepted"}, - status=status.HTTP_200_OK, - ) - - # Workspace invitation rejected - return Response( - {"message": "Workspace Invitation was not accepted"}, - status=status.HTTP_200_OK, - ) - - return Response( - {"error": "You have already responded to the invitation request"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def get(self, request, slug, pk): - workspace_invitation = WorkspaceMemberInvite.objects.get( - workspace__slug=slug, pk=pk - ) - serializer = WorkSpaceMemberInviteSerializer(workspace_invitation) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class UserWorkspaceInvitationsViewSet(BaseViewSet): - serializer_class = WorkSpaceMemberInviteSerializer - model = WorkspaceMemberInvite - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(email=self.request.user.email) - .select_related("workspace", "workspace__owner", "created_by") - .annotate(total_members=Count("workspace__workspace_member")) - ) - - def create(self, request): - invitations = request.data.get("invitations", []) - workspace_invitations = WorkspaceMemberInvite.objects.filter( - pk__in=invitations, email=request.user.email - ).order_by("-created_at") - - # If the user is already a member of workspace and was deactivated then activate the user - for invitation in workspace_invitations: - # Update the WorkspaceMember for this specific invitation - WorkspaceMember.objects.filter( - workspace_id=invitation.workspace_id, member=request.user - ).update(is_active=True, role=invitation.role) - - # Bulk create the user for all the workspaces - WorkspaceMember.objects.bulk_create( - [ - WorkspaceMember( - workspace=invitation.workspace, - member=request.user, - role=invitation.role, - created_by=request.user, - ) - for invitation in workspace_invitations - ], - ignore_conflicts=True, - ) - - # Delete joined workspace invites - workspace_invitations.delete() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkSpaceMemberViewSet(BaseViewSet): - serializer_class = WorkspaceMemberAdminSerializer - model = WorkspaceMember - - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get_permissions(self): - if self.action == "leave": - self.permission_classes = [ - WorkspaceUserPermission, - ] - else: - self.permission_classes = [ - WorkspaceEntityPermission, - ] - - return super(WorkSpaceMemberViewSet, self).get_permissions() - - search_fields = [ - "member__display_name", - "member__first_name", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - is_active=True, - ) - .select_related("workspace", "workspace__owner") - .select_related("member") - ) - - def list(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - member=request.user, - workspace__slug=slug, - is_active=True, - ) - - # Get all active workspace members - workspace_members = self.get_queryset() - - if workspace_member.role > 10: - serializer = WorkspaceMemberAdminSerializer( - workspace_members, - fields=("id", "member", "role"), - many=True, - ) - else: - serializer = WorkSpaceMemberSerializer( - workspace_members, - fields=("id", "member", "role"), - many=True, - ) - return Response(serializer.data, status=status.HTTP_200_OK) - - def partial_update(self, request, slug, pk): - workspace_member = WorkspaceMember.objects.get( - pk=pk, - workspace__slug=slug, - member__is_bot=False, - is_active=True, - ) - if request.user.id == workspace_member.member_id: - return Response( - {"error": "You cannot update your own role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get the requested user role - requested_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - # Check if role is being updated - # One cannot update role higher than his own role - if ( - "role" in request.data - and int(request.data.get("role", workspace_member.role)) - > requested_workspace_member.role - ): - return Response( - { - "error": "You cannot update a role that is higher than your own role" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = WorkSpaceMemberSerializer( - workspace_member, data=request.data, partial=True - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, pk): - # Check the user role who is deleting the user - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - pk=pk, - member__is_bot=False, - is_active=True, - ) - - # check requesting user role - requesting_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - - if str(workspace_member.id) == str(requesting_workspace_member.id): - return Response( - { - "error": "You cannot remove yourself from the workspace. Please use leave workspace" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if requesting_workspace_member.role < workspace_member.role: - return Response( - { - "error": "You cannot remove a user having role higher than you" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if ( - Project.objects.annotate( - total_members=Count("project_projectmember"), - member_with_role=Count( - "project_projectmember", - filter=Q( - project_projectmember__member_id=workspace_member.id, - project_projectmember__role=20, - ), - ), - ) - .filter(total_members=1, member_with_role=1, workspace__slug=slug) - .exists() - ): - return Response( - { - "error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Deactivate the users from the projects where the user is part of - _ = ProjectMember.objects.filter( - workspace__slug=slug, - member_id=workspace_member.member_id, - is_active=True, - ).update(is_active=False) - - workspace_member.is_active = False - workspace_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - def leave(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - - # Check if the leaving user is the only admin of the workspace - if ( - workspace_member.role == 20 - and not WorkspaceMember.objects.filter( - workspace__slug=slug, - role=20, - is_active=True, - ).count() - > 1 - ): - return Response( - { - "error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if ( - Project.objects.annotate( - total_members=Count("project_projectmember"), - member_with_role=Count( - "project_projectmember", - filter=Q( - project_projectmember__member_id=request.user.id, - project_projectmember__role=20, - ), - ), - ) - .filter(total_members=1, member_with_role=1, workspace__slug=slug) - .exists() - ): - return Response( - { - "error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # # Deactivate the users from the projects where the user is part of - _ = ProjectMember.objects.filter( - workspace__slug=slug, - member_id=workspace_member.member_id, - is_active=True, - ).update(is_active=False) - - # # Deactivate the user - workspace_member.is_active = False - workspace_member.save() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class WorkspaceProjectMemberEndpoint(BaseAPIView): - serializer_class = ProjectMemberRoleSerializer - model = ProjectMember - - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get(self, request, slug): - # Fetch all project IDs where the user is involved - project_ids = ( - ProjectMember.objects.filter( - member=request.user, - is_active=True, - ) - .values_list("project_id", flat=True) - .distinct() - ) - - # Get all the project members in which the user is involved - project_members = ProjectMember.objects.filter( - workspace__slug=slug, - project_id__in=project_ids, - is_active=True, - ).select_related("project", "member", "workspace") - project_members = ProjectMemberRoleSerializer( - project_members, many=True - ).data - - project_members_dict = dict() - - # Construct a dictionary with project_id as key and project_members as value - for project_member in project_members: - project_id = project_member.pop("project") - if str(project_id) not in project_members_dict: - project_members_dict[str(project_id)] = [] - project_members_dict[str(project_id)].append(project_member) - - return Response(project_members_dict, status=status.HTTP_200_OK) - - -class TeamMemberViewSet(BaseViewSet): - serializer_class = TeamSerializer - model = Team - permission_classes = [ - WorkSpaceAdminPermission, - ] - - search_fields = [ - "member__display_name", - "member__first_name", - ] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "workspace__owner") - .prefetch_related("members") - ) - - def create(self, request, slug): - members = list( - WorkspaceMember.objects.filter( - workspace__slug=slug, - member__id__in=request.data.get("members", []), - is_active=True, - ) - .annotate(member_str_id=Cast("member", output_field=CharField())) - .distinct() - .values_list("member_str_id", flat=True) - ) - - if len(members) != len(request.data.get("members", [])): - users = list( - set(request.data.get("members", [])).difference(members) - ) - users = User.objects.filter(pk__in=users) - - serializer = UserLiteSerializer(users, many=True) - return Response( - { - "error": f"{len(users)} of the member(s) are not a part of the workspace", - "members": serializer.data, - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - serializer = TeamSerializer( - data=request.data, context={"workspace": workspace} - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): - def get(self, request): - user = User.objects.get(pk=request.user.id) - - last_workspace_id = user.last_workspace_id - - if last_workspace_id is None: - return Response( - { - "project_details": [], - "workspace_details": {}, - }, - status=status.HTTP_200_OK, - ) - - workspace = Workspace.objects.get(pk=last_workspace_id) - workspace_serializer = WorkSpaceSerializer(workspace) - - project_member = ProjectMember.objects.filter( - workspace_id=last_workspace_id, member=request.user - ).select_related("workspace", "project", "member", "workspace__owner") - - project_member_serializer = ProjectMemberSerializer( - project_member, many=True - ) - - return Response( - { - "workspace_details": workspace_serializer.data, - "project_details": project_member_serializer.data, - }, - status=status.HTTP_200_OK, - ) - - -class WorkspaceMemberUserEndpoint(BaseAPIView): - def get(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - member=request.user, - workspace__slug=slug, - is_active=True, - ) - serializer = WorkspaceMemberMeSerializer(workspace_member) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class WorkspaceMemberUserViewsEndpoint(BaseAPIView): - def post(self, request, slug): - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - workspace_member.view_props = request.data.get("view_props", {}) - workspace_member.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - - -class UserActivityGraphEndpoint(BaseAPIView): - def get(self, request, slug): - issue_activities = ( - IssueActivity.objects.filter( - actor=request.user, - workspace__slug=slug, - created_at__date__gte=date.today() + relativedelta(months=-6), - ) - .annotate(created_date=Cast("created_at", DateField())) - .values("created_date") - .annotate(activity_count=Count("created_date")) - .order_by("created_date") - ) - - return Response(issue_activities, status=status.HTTP_200_OK) - - -class UserIssueCompletedGraphEndpoint(BaseAPIView): - def get(self, request, slug): - month = request.GET.get("month", 1) - - issues = ( - Issue.issue_objects.filter( - assignees__in=[request.user], - workspace__slug=slug, - completed_at__month=month, - completed_at__isnull=False, - ) - .annotate(completed_week=ExtractWeek("completed_at")) - .annotate(week=F("completed_week") % 4) - .values("week") - .annotate(completed_count=Count("completed_week")) - .order_by("week") - ) - - return Response(issues, status=status.HTTP_200_OK) - - -class WeekInMonth(Func): - function = "FLOOR" - template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER" - - -class UserWorkspaceDashboardEndpoint(BaseAPIView): - def get(self, request, slug): - issue_activities = ( - IssueActivity.objects.filter( - actor=request.user, - workspace__slug=slug, - created_at__date__gte=date.today() + relativedelta(months=-3), - ) - .annotate(created_date=Cast("created_at", DateField())) - .values("created_date") - .annotate(activity_count=Count("created_date")) - .order_by("created_date") - ) - - month = request.GET.get("month", 1) - - completed_issues = ( - Issue.issue_objects.filter( - assignees__in=[request.user], - workspace__slug=slug, - completed_at__month=month, - completed_at__isnull=False, - ) - .annotate(day_of_month=ExtractDay("completed_at")) - .annotate(week_in_month=WeekInMonth(F("day_of_month"))) - .values("week_in_month") - .annotate(completed_count=Count("id")) - .order_by("week_in_month") - ) - - assigned_issues = Issue.issue_objects.filter( - workspace__slug=slug, assignees__in=[request.user] - ).count() - - pending_issues_count = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[request.user], - ).count() - - completed_issues_count = Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[request.user], - state__group="completed", - ).count() - - issues_due_week = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[request.user], - ) - .annotate(target_week=ExtractWeek("target_date")) - .filter(target_week=timezone.now().date().isocalendar()[1]) - .count() - ) - - state_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, assignees__in=[request.user] - ) - .annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) - - overdue_issues = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[request.user], - target_date__lt=timezone.now(), - completed_at__isnull=True, - ).values("id", "name", "workspace__slug", "project_id", "target_date") - - upcoming_issues = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - start_date__gte=timezone.now(), - workspace__slug=slug, - assignees__in=[request.user], - completed_at__isnull=True, - ).values("id", "name", "workspace__slug", "project_id", "start_date") - - return Response( - { - "issue_activities": issue_activities, - "completed_issues": completed_issues, - "assigned_issues_count": assigned_issues, - "pending_issues_count": pending_issues_count, - "completed_issues_count": completed_issues_count, - "issues_due_week_count": issues_due_week, - "state_distribution": state_distribution, - "overdue_issues": overdue_issues, - "upcoming_issues": upcoming_issues, - }, - status=status.HTTP_200_OK, - ) - - -class WorkspaceThemeViewSet(BaseViewSet): - permission_classes = [ - WorkSpaceAdminPermission, - ] - model = WorkspaceTheme - serializer_class = WorkspaceThemeSerializer - - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - ) - - def create(self, request, slug): - workspace = Workspace.objects.get(slug=slug) - serializer = WorkspaceThemeSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(workspace=workspace, actor=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class WorkspaceUserProfileStatsEndpoint(BaseAPIView): - def get(self, request, slug, user_id): - filters = issue_filters(request.query_params, "GET") - - state_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True - ) - .filter(**filters) - .annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) - - priority_order = ["urgent", "high", "medium", "low", "none"] - - priority_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True - ) - .filter(**filters) - .values("priority") - .annotate(priority_count=Count("priority")) - .filter(priority_count__gte=1) - .annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - default=Value(len(priority_order)), - output_field=IntegerField(), - ) - ) - .order_by("priority_order") - ) - - created_issues = ( - Issue.issue_objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - created_by_id=user_id, - ) - .filter(**filters) - .count() - ) - - assigned_issues_count = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - pending_issues_count = ( - Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - ) - .filter(**filters) - .count() - ) - - completed_issues_count = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - state__group="completed", - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True - ) - .filter(**filters) - .count() - ) - - subscribed_issues_count = ( - IssueSubscriber.objects.filter( - workspace__slug=slug, - subscriber_id=user_id, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True - ) - .filter(**filters) - .count() - ) - - upcoming_cycles = CycleIssue.objects.filter( - workspace__slug=slug, - cycle__start_date__gt=timezone.now().date(), - issue__assignees__in=[ - user_id, - ], - ).values("cycle__name", "cycle__id", "cycle__project_id") - - present_cycle = CycleIssue.objects.filter( - workspace__slug=slug, - cycle__start_date__lt=timezone.now().date(), - cycle__end_date__gt=timezone.now().date(), - issue__assignees__in=[ - user_id, - ], - ).values("cycle__name", "cycle__id", "cycle__project_id") - - return Response( - { - "state_distribution": state_distribution, - "priority_distribution": priority_distribution, - "created_issues": created_issues, - "assigned_issues": assigned_issues_count, - "completed_issues": completed_issues_count, - "pending_issues": pending_issues_count, - "subscribed_issues": subscribed_issues_count, - "present_cycles": present_cycle, - "upcoming_cycles": upcoming_cycles, - } - ) - - -class WorkspaceUserActivityEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get(self, request, slug, user_id): - projects = request.query_params.getlist("project", []) - - queryset = IssueActivity.objects.filter( - ~Q(field__in=["comment", "vote", "reaction", "draft"]), - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, - actor=user_id, - ).select_related("actor", "workspace", "issue", "project") - - if projects: - queryset = queryset.filter(project__in=projects) - - return self.paginate( - request=request, - queryset=queryset, - on_results=lambda issue_activities: IssueActivitySerializer( - issue_activities, many=True - ).data, - ) - - -class WorkspaceUserProfileEndpoint(BaseAPIView): - def get(self, request, slug, user_id): - user_data = User.objects.get(pk=user_id) - - requesting_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, - member=request.user, - is_active=True, - ) - projects = [] - if requesting_workspace_member.role >= 10: - projects = ( - Project.objects.filter( - workspace__slug=slug, - project_projectmember__member=request.user, - project_projectmember__is_active=True, - ) - .annotate( - created_issues=Count( - "project_issue", - filter=Q( - project_issue__created_by_id=user_id, - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .annotate( - assigned_issues=Count( - "project_issue", - filter=Q( - project_issue__assignees__in=[user_id], - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .annotate( - completed_issues=Count( - "project_issue", - filter=Q( - project_issue__completed_at__isnull=False, - project_issue__assignees__in=[user_id], - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "project_issue", - filter=Q( - project_issue__state__group__in=[ - "backlog", - "unstarted", - "started", - ], - project_issue__assignees__in=[user_id], - project_issue__archived_at__isnull=True, - project_issue__is_draft=False, - ), - ) - ) - .values( - "id", - "name", - "identifier", - "emoji", - "icon_prop", - "created_issues", - "assigned_issues", - "completed_issues", - "pending_issues", - ) - ) - - return Response( - { - "project_data": projects, - "user_data": { - "email": user_data.email, - "first_name": user_data.first_name, - "last_name": user_data.last_name, - "avatar": user_data.avatar, - "cover_image": user_data.cover_image, - "date_joined": user_data.date_joined, - "user_timezone": user_data.user_timezone, - "display_name": user_data.display_name, - }, - }, - status=status.HTTP_200_OK, - ) - - -class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug, user_id): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - filters = issue_filters(request.query_params, "GET") - - order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = ( - Issue.issue_objects.filter( - Q(assignees__in=[user_id]) - | Q(created_by_id=user_id) - | Q(issue_subscribers__subscriber_id=user_id), - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True - ) - .filter(**filters) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .order_by("created_at") - ).distinct() - - issue_queryset = order_by_param( - issue_queryset=issue_queryset, order_by_param=order_by_param - ) - - issues = IssueSerializer( - issue_queryset, many=True, fields=fields if fields else None - ).data - return Response(issues, status=status.HTTP_200_OK) - - -class WorkspaceLabelsEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug): - labels = Label.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True - ) - serializer = LabelSerializer(labels, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceStatesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get(self, request, slug): - states = State.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True - ) - serializer = StateSerializer(states, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceEstimatesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] - - def get(self, request, slug): - estimate_ids = Project.objects.filter( - workspace__slug=slug, estimate__isnull=False - ).values_list("estimate_id", flat=True) - estimates = Estimate.objects.filter( - pk__in=estimate_ids - ).prefetch_related( - Prefetch( - "points", - queryset=EstimatePoint.objects.select_related( - "estimate", "workspace", "project" - ), - ) - ) - serializer = WorkspaceEstimateSerializer(estimates, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class WorkspaceModulesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug): - modules = ( - Module.objects.filter(workspace__slug=slug) - .select_related("project") - .select_related("workspace") - .select_related("lead") - .prefetch_related("members") - .prefetch_related( - Prefetch( - "link_module", - queryset=ModuleLink.objects.select_related( - "module", "created_by" - ), - ) - ) - .annotate( - total_issues=Count( - "issue_module", - filter=Q( - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ), - ) - .annotate( - completed_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="completed", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="cancelled", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="started", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="unstarted", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="backlog", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ) - ) - .order_by(self.kwargs.get("order_by", "-created_at")) - ) - - serializer = ModuleSerializer(modules, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceCyclesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def get(self, request, slug): - cycles = ( - Cycle.objects.filter(workspace__slug=slug) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="completed", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="cancelled", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="unstarted", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q( - issue_cycle__issue__state__group="backlog", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - total_estimates=Sum("issue_cycle__issue__estimate_point") - ) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q( - issue_cycle__issue__state__group="completed", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .order_by(self.kwargs.get("order_by", "-created_at")) - .distinct() - ) - serializer = CycleSerializer(cycles, many=True).data - return Response(serializer, status=status.HTTP_200_OK) - - -class WorkspaceUserPropertiesEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceViewerPermission, - ] - - def patch(self, request, slug): - workspace_properties = WorkspaceUserProperties.objects.get( - user=request.user, - workspace__slug=slug, - ) - - workspace_properties.filters = request.data.get( - "filters", workspace_properties.filters - ) - workspace_properties.display_filters = request.data.get( - "display_filters", workspace_properties.display_filters - ) - workspace_properties.display_properties = request.data.get( - "display_properties", workspace_properties.display_properties - ) - workspace_properties.save() - - serializer = WorkspaceUserPropertiesSerializer(workspace_properties) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def get(self, request, slug): - ( - workspace_properties, - _, - ) = WorkspaceUserProperties.objects.get_or_create( - user=request.user, workspace__slug=slug - ) - serializer = WorkspaceUserPropertiesSerializer(workspace_properties) - return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/base.py b/apiserver/plane/app/views/workspace/base.py new file mode 100644 index 000000000..0fb8f2d80 --- /dev/null +++ b/apiserver/plane/app/views/workspace/base.py @@ -0,0 +1,414 @@ +# Python imports +from datetime import date +from dateutil.relativedelta import relativedelta +import csv +import io + + +# Django imports +from django.http import HttpResponse +from django.db import IntegrityError +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Count, +) +from django.db.models.functions import ExtractWeek, Cast, ExtractDay +from django.db.models.fields import DateField + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import ( + WorkSpaceSerializer, + WorkspaceThemeSerializer, +) +from plane.app.views.base import BaseViewSet, BaseAPIView +from plane.db.models import ( + Workspace, + IssueActivity, + Issue, + WorkspaceTheme, + WorkspaceMember, +) +from plane.app.permissions import ( + WorkSpaceBasePermission, + WorkSpaceAdminPermission, + WorkspaceEntityPermission, +) +from plane.utils.cache import cache_response, invalidate_cache + +class WorkSpaceViewSet(BaseViewSet): + model = Workspace + serializer_class = WorkSpaceSerializer + permission_classes = [ + WorkSpaceBasePermission, + ] + + search_fields = [ + "name", + ] + filterset_fields = [ + "owner", + ] + + lookup_field = "slug" + + def get_queryset(self): + member_count = ( + WorkspaceMember.objects.filter( + workspace=OuterRef("id"), + member__is_bot=False, + is_active=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + + issue_count = ( + Issue.issue_objects.filter(workspace=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + return ( + self.filter_queryset( + super().get_queryset().select_related("owner") + ) + .order_by("name") + .filter( + workspace_member__member=self.request.user, + workspace_member__is_active=True, + ) + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + .select_related("owner") + ) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def create(self, request): + try: + serializer = WorkSpaceSerializer(data=request.data) + + slug = request.data.get("slug", False) + name = request.data.get("name", False) + + if not name or not slug: + return Response( + {"error": "Both name and slug are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if len(name) > 80 or len(slug) > 48: + return Response( + { + "error": "The maximum length for name is 80 and for slug is 48" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if serializer.is_valid(): + serializer.save(owner=request.user) + # Create Workspace member + _ = WorkspaceMember.objects.create( + workspace_id=serializer.data["id"], + member=request.user, + role=20, + company_role=request.data.get("company_role", ""), + ) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + [serializer.errors[error][0] for error in serializer.errors], + status=status.HTTP_400_BAD_REQUEST, + ) + + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"slug": "The workspace with the slug already exists"}, + status=status.HTTP_410_GONE, + ) + @cache_response(60 * 60 * 2) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) + + +class UserWorkSpacesEndpoint(BaseAPIView): + search_fields = [ + "name", + ] + filterset_fields = [ + "owner", + ] + + @cache_response(60 * 60 * 2) + def get(self, request): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + member_count = ( + WorkspaceMember.objects.filter( + workspace=OuterRef("id"), + member__is_bot=False, + is_active=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + + issue_count = ( + Issue.issue_objects.filter(workspace=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + + workspace = ( + Workspace.objects.prefetch_related( + Prefetch( + "workspace_member", + queryset=WorkspaceMember.objects.filter( + member=request.user, is_active=True + ), + ) + ) + .select_related("owner") + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + .filter( + workspace_member__member=request.user, + workspace_member__is_active=True, + ) + .distinct() + ) + workspaces = WorkSpaceSerializer( + self.filter_queryset(workspace), + fields=fields if fields else None, + many=True, + ).data + return Response(workspaces, status=status.HTTP_200_OK) + + +class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): + def get(self, request): + slug = request.GET.get("slug", False) + + if not slug or slug == "": + return Response( + {"error": "Workspace Slug is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.filter(slug=slug).exists() + return Response({"status": not workspace}, status=status.HTTP_200_OK) + + +class WeekInMonth(Func): + function = "FLOOR" + template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER" + + +class UserWorkspaceDashboardEndpoint(BaseAPIView): + def get(self, request, slug): + issue_activities = ( + IssueActivity.objects.filter( + actor=request.user, + workspace__slug=slug, + created_at__date__gte=date.today() + relativedelta(months=-3), + ) + .annotate(created_date=Cast("created_at", DateField())) + .values("created_date") + .annotate(activity_count=Count("created_date")) + .order_by("created_date") + ) + + month = request.GET.get("month", 1) + + completed_issues = ( + Issue.issue_objects.filter( + assignees__in=[request.user], + workspace__slug=slug, + completed_at__month=month, + completed_at__isnull=False, + ) + .annotate(day_of_month=ExtractDay("completed_at")) + .annotate(week_in_month=WeekInMonth(F("day_of_month"))) + .values("week_in_month") + .annotate(completed_count=Count("id")) + .order_by("week_in_month") + ) + + assigned_issues = Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] + ).count() + + pending_issues_count = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[request.user], + ).count() + + completed_issues_count = Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[request.user], + state__group="completed", + ).count() + + issues_due_week = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[request.user], + ) + .annotate(target_week=ExtractWeek("target_date")) + .filter(target_week=timezone.now().date().isocalendar()[1]) + .count() + ) + + state_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] + ) + .annotate(state_group=F("state__group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) + + overdue_issues = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[request.user], + target_date__lt=timezone.now(), + completed_at__isnull=True, + ).values("id", "name", "workspace__slug", "project_id", "target_date") + + upcoming_issues = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + start_date__gte=timezone.now(), + workspace__slug=slug, + assignees__in=[request.user], + completed_at__isnull=True, + ).values("id", "name", "workspace__slug", "project_id", "start_date") + + return Response( + { + "issue_activities": issue_activities, + "completed_issues": completed_issues, + "assigned_issues_count": assigned_issues, + "pending_issues_count": pending_issues_count, + "completed_issues_count": completed_issues_count, + "issues_due_week_count": issues_due_week, + "state_distribution": state_distribution, + "overdue_issues": overdue_issues, + "upcoming_issues": upcoming_issues, + }, + status=status.HTTP_200_OK, + ) + + +class WorkspaceThemeViewSet(BaseViewSet): + permission_classes = [ + WorkSpaceAdminPermission, + ] + model = WorkspaceTheme + serializer_class = WorkspaceThemeSerializer + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + ) + + def create(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + serializer = WorkspaceThemeSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(workspace=workspace, actor=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class ExportWorkspaceUserActivityEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def generate_csv_from_rows(self, rows): + """Generate CSV buffer from rows.""" + csv_buffer = io.StringIO() + writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) + [writer.writerow(row) for row in rows] + csv_buffer.seek(0) + return csv_buffer + + def post(self, request, slug, user_id): + + if not request.data.get("date"): + return Response( + {"error": "Date is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user_activities = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + created_at__date=request.data.get("date"), + project__project_projectmember__member=request.user, + actor_id=user_id, + ).select_related("actor", "workspace", "issue", "project")[:10000] + + header = [ + "Actor name", + "Issue ID", + "Project", + "Created at", + "Updated at", + "Action", + "Field", + "Old value", + "New value", + ] + rows = [ + ( + activity.actor.display_name, + f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}", + activity.project.name, + activity.created_at, + activity.updated_at, + activity.verb, + activity.field, + activity.old_value, + activity.new_value, + ) + for activity in user_activities + ] + csv_buffer = self.generate_csv_from_rows([header] + rows) + response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv") + response["Content-Disposition"] = ( + 'attachment; filename="workspace-user-activity.csv"' + ) + return response diff --git a/apiserver/plane/app/views/workspace/cycle.py b/apiserver/plane/app/views/workspace/cycle.py new file mode 100644 index 000000000..ea081cf99 --- /dev/null +++ b/apiserver/plane/app/views/workspace/cycle.py @@ -0,0 +1,116 @@ +# Django imports +from django.db.models import ( + Q, + Count, + Sum, +) + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.views.base import BaseAPIView +from plane.db.models import Cycle +from plane.app.permissions import WorkspaceViewerPermission +from plane.app.serializers.cycle import CycleSerializer + + +class WorkspaceCyclesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def get(self, request, slug): + cycles = ( + Cycle.objects.filter(workspace__slug=slug) + .select_related("project") + .select_related("workspace") + .select_related("owned_by") + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="cancelled", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="unstarted", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="backlog", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + total_estimates=Sum("issue_cycle__issue__estimate_point") + ) + .annotate( + completed_estimates=Sum( + "issue_cycle__issue__estimate_point", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_estimates=Sum( + "issue_cycle__issue__estimate_point", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .order_by(self.kwargs.get("order_by", "-created_at")) + .distinct() + ) + serializer = CycleSerializer(cycles, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/estimate.py b/apiserver/plane/app/views/workspace/estimate.py new file mode 100644 index 000000000..6b64d8c90 --- /dev/null +++ b/apiserver/plane/app/views/workspace/estimate.py @@ -0,0 +1,39 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import WorkspaceEstimateSerializer +from plane.app.views.base import BaseAPIView +from plane.db.models import Project, Estimate +from plane.app.permissions import WorkspaceEntityPermission + +# Django imports +from django.db.models import ( + Prefetch, +) +from plane.utils.cache import cache_response + + +class WorkspaceEstimatesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + @cache_response(60 * 60 * 2) + def get(self, request, slug): + estimate_ids = Project.objects.filter( + workspace__slug=slug, estimate__isnull=False + ).values_list("estimate_id", flat=True) + estimates = Estimate.objects.filter( + pk__in=estimate_ids + ).prefetch_related( + Prefetch( + "points", + queryset=Project.objects.select_related( + "estimate", "workspace", "project" + ), + ) + ) + serializer = WorkspaceEstimateSerializer(estimates, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/invite.py b/apiserver/plane/app/views/workspace/invite.py new file mode 100644 index 000000000..807c060ad --- /dev/null +++ b/apiserver/plane/app/views/workspace/invite.py @@ -0,0 +1,301 @@ +# Python imports +import jwt +from datetime import datetime + +# Django imports +from django.conf import settings +from django.utils import timezone +from django.db.models import Count +from django.core.exceptions import ValidationError +from django.core.validators import validate_email + +# Third party modules +from rest_framework import status +from rest_framework.response import Response +from rest_framework.permissions import AllowAny + +# Module imports +from plane.app.serializers import ( + WorkSpaceMemberSerializer, + WorkSpaceMemberInviteSerializer, +) +from plane.app.views.base import BaseAPIView +from .. import BaseViewSet +from plane.db.models import ( + User, + Workspace, + WorkspaceMemberInvite, + WorkspaceMember, +) +from plane.app.permissions import WorkSpaceAdminPermission +from plane.bgtasks.workspace_invitation_task import workspace_invitation +from plane.bgtasks.event_tracking_task import workspace_invite_event +from plane.utils.cache import invalidate_cache + +class WorkspaceInvitationsViewset(BaseViewSet): + """Endpoint for creating, listing and deleting workspaces""" + + serializer_class = WorkSpaceMemberInviteSerializer + model = WorkspaceMemberInvite + + permission_classes = [ + WorkSpaceAdminPermission, + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "workspace__owner", "created_by") + ) + + def create(self, request, slug): + emails = request.data.get("emails", []) + # Check if email is provided + if not emails: + return Response( + {"error": "Emails are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # check for role level of the requesting user + requesting_user = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + + # Check if any invited user has an higher role + if len( + [ + email + for email in emails + if int(email.get("role", 10)) > requesting_user.role + ] + ): + return Response( + {"error": "You cannot invite a user with higher role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the workspace object + workspace = Workspace.objects.get(slug=slug) + + # Check if user is already a member of workspace + workspace_members = WorkspaceMember.objects.filter( + workspace_id=workspace.id, + member__email__in=[email.get("email") for email in emails], + is_active=True, + ).select_related("member", "workspace", "workspace__owner") + + if workspace_members: + return Response( + { + "error": "Some users are already member of workspace", + "workspace_users": WorkSpaceMemberSerializer( + workspace_members, many=True + ).data, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace_invitations = [] + for email in emails: + try: + validate_email(email.get("email")) + workspace_invitations.append( + WorkspaceMemberInvite( + email=email.get("email").strip().lower(), + workspace_id=workspace.id, + token=jwt.encode( + { + "email": email, + "timestamp": datetime.now().timestamp(), + }, + settings.SECRET_KEY, + algorithm="HS256", + ), + role=email.get("role", 10), + created_by=request.user, + ) + ) + except ValidationError: + return Response( + { + "error": f"Invalid email - {email} provided a valid email address is required to send the invite" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Create workspace member invite + workspace_invitations = WorkspaceMemberInvite.objects.bulk_create( + workspace_invitations, batch_size=10, ignore_conflicts=True + ) + + current_site = request.META.get("HTTP_ORIGIN") + + # Send invitations + for invitation in workspace_invitations: + workspace_invitation.delay( + invitation.email, + workspace.id, + invitation.token, + current_site, + request.user.email, + ) + + return Response( + { + "message": "Emails sent successfully", + }, + status=status.HTTP_200_OK, + ) + + def destroy(self, request, slug, pk): + workspace_member_invite = WorkspaceMemberInvite.objects.get( + pk=pk, workspace__slug=slug + ) + workspace_member_invite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceJoinEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + """Invitation response endpoint the user can respond to the invitation""" + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + def post(self, request, slug, pk): + workspace_invite = WorkspaceMemberInvite.objects.get( + pk=pk, workspace__slug=slug + ) + + email = request.data.get("email", "") + + # Check the email + if email == "" or workspace_invite.email != email: + return Response( + {"error": "You do not have permission to join the workspace"}, + status=status.HTTP_403_FORBIDDEN, + ) + + # If already responded then return error + if workspace_invite.responded_at is None: + workspace_invite.accepted = request.data.get("accepted", False) + workspace_invite.responded_at = timezone.now() + workspace_invite.save() + + if workspace_invite.accepted: + # Check if the user created account after invitation + user = User.objects.filter(email=email).first() + + # If the user is present then create the workspace member + if user is not None: + # Check if the user was already a member of workspace then activate the user + workspace_member = WorkspaceMember.objects.filter( + workspace=workspace_invite.workspace, member=user + ).first() + if workspace_member is not None: + workspace_member.is_active = True + workspace_member.role = workspace_invite.role + workspace_member.save() + else: + # Create a Workspace + _ = WorkspaceMember.objects.create( + workspace=workspace_invite.workspace, + member=user, + role=workspace_invite.role, + ) + + # Set the user last_workspace_id to the accepted workspace + user.last_workspace_id = workspace_invite.workspace.id + user.save() + + # Delete the invitation + workspace_invite.delete() + + # Send event + workspace_invite_event.delay( + user=user.id if user is not None else None, + email=email, + user_agent=request.META.get("HTTP_USER_AGENT"), + ip=request.META.get("REMOTE_ADDR"), + event_name="MEMBER_ACCEPTED", + accepted_from="EMAIL", + ) + + return Response( + {"message": "Workspace Invitation Accepted"}, + status=status.HTTP_200_OK, + ) + + # Workspace invitation rejected + return Response( + {"message": "Workspace Invitation was not accepted"}, + status=status.HTTP_200_OK, + ) + + return Response( + {"error": "You have already responded to the invitation request"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get(self, request, slug, pk): + workspace_invitation = WorkspaceMemberInvite.objects.get( + workspace__slug=slug, pk=pk + ) + serializer = WorkSpaceMemberInviteSerializer(workspace_invitation) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class UserWorkspaceInvitationsViewSet(BaseViewSet): + serializer_class = WorkSpaceMemberInviteSerializer + model = WorkspaceMemberInvite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(email=self.request.user.email) + .select_related("workspace", "workspace__owner", "created_by") + .annotate(total_members=Count("workspace__workspace_member")) + ) + + @invalidate_cache(path="/api/workspaces/", user=False) + @invalidate_cache(path="/api/users/me/workspaces/") + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def create(self, request): + invitations = request.data.get("invitations", []) + workspace_invitations = WorkspaceMemberInvite.objects.filter( + pk__in=invitations, email=request.user.email + ).order_by("-created_at") + + # If the user is already a member of workspace and was deactivated then activate the user + for invitation in workspace_invitations: + # Update the WorkspaceMember for this specific invitation + WorkspaceMember.objects.filter( + workspace_id=invitation.workspace_id, member=request.user + ).update(is_active=True, role=invitation.role) + + # Bulk create the user for all the workspaces + WorkspaceMember.objects.bulk_create( + [ + WorkspaceMember( + workspace=invitation.workspace, + member=request.user, + role=invitation.role, + created_by=request.user, + ) + for invitation in workspace_invitations + ], + ignore_conflicts=True, + ) + + # Delete joined workspace invites + workspace_invitations.delete() + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/workspace/label.py b/apiserver/plane/app/views/workspace/label.py new file mode 100644 index 000000000..ba396a842 --- /dev/null +++ b/apiserver/plane/app/views/workspace/label.py @@ -0,0 +1,25 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import LabelSerializer +from plane.app.views.base import BaseAPIView +from plane.db.models import Label +from plane.app.permissions import WorkspaceViewerPermission +from plane.utils.cache import cache_response + +class WorkspaceLabelsEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + @cache_response(60 * 60 * 2) + def get(self, request, slug): + labels = Label.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + serializer = LabelSerializer(labels, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py new file mode 100644 index 000000000..ff88e47f8 --- /dev/null +++ b/apiserver/plane/app/views/workspace/member.py @@ -0,0 +1,396 @@ +# Django imports +from django.db.models import ( + Q, + Count, +) +from django.db.models.functions import Cast +from django.db.models import CharField + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import ( + WorkSpaceMemberSerializer, + TeamSerializer, + UserLiteSerializer, + WorkspaceMemberAdminSerializer, + WorkspaceMemberMeSerializer, + ProjectMemberRoleSerializer, +) +from plane.app.views.base import BaseAPIView +from .. import BaseViewSet +from plane.db.models import ( + User, + Workspace, + Team, + ProjectMember, + Project, + WorkspaceMember, +) +from plane.app.permissions import ( + WorkSpaceAdminPermission, + WorkspaceEntityPermission, + WorkspaceUserPermission, +) +from plane.utils.cache import cache_response, invalidate_cache + + +class WorkSpaceMemberViewSet(BaseViewSet): + serializer_class = WorkspaceMemberAdminSerializer + model = WorkspaceMember + + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get_permissions(self): + if self.action == "leave": + self.permission_classes = [ + WorkspaceUserPermission, + ] + else: + self.permission_classes = [ + WorkspaceEntityPermission, + ] + + return super(WorkSpaceMemberViewSet, self).get_permissions() + + search_fields = [ + "member__display_name", + "member__first_name", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + is_active=True, + ) + .select_related("workspace", "workspace__owner") + .select_related("member") + ) + + @cache_response(60 * 60 * 2) + def list(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + member=request.user, + workspace__slug=slug, + is_active=True, + ) + + # Get all active workspace members + workspace_members = self.get_queryset() + + if workspace_member.role > 10: + serializer = WorkspaceMemberAdminSerializer( + workspace_members, + fields=("id", "member", "role"), + many=True, + ) + else: + serializer = WorkSpaceMemberSerializer( + workspace_members, + fields=("id", "member", "role"), + many=True, + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def partial_update(self, request, slug, pk): + workspace_member = WorkspaceMember.objects.get( + pk=pk, + workspace__slug=slug, + member__is_bot=False, + is_active=True, + ) + if request.user.id == workspace_member.member_id: + return Response( + {"error": "You cannot update your own role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the requested user role + requested_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + # Check if role is being updated + # One cannot update role higher than his own role + if ( + "role" in request.data + and int(request.data.get("role", workspace_member.role)) + > requested_workspace_member.role + ): + return Response( + { + "error": "You cannot update a role that is higher than your own role" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = WorkSpaceMemberSerializer( + workspace_member, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def destroy(self, request, slug, pk): + # Check the user role who is deleting the user + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + pk=pk, + member__is_bot=False, + is_active=True, + ) + + # check requesting user role + requesting_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + + if str(workspace_member.id) == str(requesting_workspace_member.id): + return Response( + { + "error": "You cannot remove yourself from the workspace. Please use leave workspace" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if requesting_workspace_member.role < workspace_member.role: + return Response( + { + "error": "You cannot remove a user having role higher than you" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if ( + Project.objects.annotate( + total_members=Count("project_projectmember"), + member_with_role=Count( + "project_projectmember", + filter=Q( + project_projectmember__member_id=workspace_member.id, + project_projectmember__role=20, + ), + ), + ) + .filter(total_members=1, member_with_role=1, workspace__slug=slug) + .exists() + ): + return Response( + { + "error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Deactivate the users from the projects where the user is part of + _ = ProjectMember.objects.filter( + workspace__slug=slug, + member_id=workspace_member.member_id, + is_active=True, + ).update(is_active=False) + + workspace_member.is_active = False + workspace_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + @invalidate_cache( + path="/api/workspaces/:slug/members/", url_params=True, user=False + ) + def leave(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + + # Check if the leaving user is the only admin of the workspace + if ( + workspace_member.role == 20 + and not WorkspaceMember.objects.filter( + workspace__slug=slug, + role=20, + is_active=True, + ).count() + > 1 + ): + return Response( + { + "error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if ( + Project.objects.annotate( + total_members=Count("project_projectmember"), + member_with_role=Count( + "project_projectmember", + filter=Q( + project_projectmember__member_id=request.user.id, + project_projectmember__role=20, + ), + ), + ) + .filter(total_members=1, member_with_role=1, workspace__slug=slug) + .exists() + ): + return Response( + { + "error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # # Deactivate the users from the projects where the user is part of + _ = ProjectMember.objects.filter( + workspace__slug=slug, + member_id=workspace_member.member_id, + is_active=True, + ).update(is_active=False) + + # # Deactivate the user + workspace_member.is_active = False + workspace_member.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceMemberUserViewsEndpoint(BaseAPIView): + def post(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + workspace_member.view_props = request.data.get("view_props", {}) + workspace_member.save() + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceMemberUserEndpoint(BaseAPIView): + def get(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + member=request.user, + workspace__slug=slug, + is_active=True, + ) + serializer = WorkspaceMemberMeSerializer(workspace_member) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class WorkspaceProjectMemberEndpoint(BaseAPIView): + serializer_class = ProjectMemberRoleSerializer + model = ProjectMember + + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug): + # Fetch all project IDs where the user is involved + project_ids = ( + ProjectMember.objects.filter( + member=request.user, + is_active=True, + ) + .values_list("project_id", flat=True) + .distinct() + ) + + # Get all the project members in which the user is involved + project_members = ProjectMember.objects.filter( + workspace__slug=slug, + project_id__in=project_ids, + is_active=True, + ).select_related("project", "member", "workspace") + project_members = ProjectMemberRoleSerializer( + project_members, many=True + ).data + + project_members_dict = dict() + + # Construct a dictionary with project_id as key and project_members as value + for project_member in project_members: + project_id = project_member.pop("project") + if str(project_id) not in project_members_dict: + project_members_dict[str(project_id)] = [] + project_members_dict[str(project_id)].append(project_member) + + return Response(project_members_dict, status=status.HTTP_200_OK) + + +class TeamMemberViewSet(BaseViewSet): + serializer_class = TeamSerializer + model = Team + permission_classes = [ + WorkSpaceAdminPermission, + ] + + search_fields = [ + "member__display_name", + "member__first_name", + ] + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace", "workspace__owner") + .prefetch_related("members") + ) + + def create(self, request, slug): + members = list( + WorkspaceMember.objects.filter( + workspace__slug=slug, + member__id__in=request.data.get("members", []), + is_active=True, + ) + .annotate(member_str_id=Cast("member", output_field=CharField())) + .distinct() + .values_list("member_str_id", flat=True) + ) + + if len(members) != len(request.data.get("members", [])): + users = list( + set(request.data.get("members", [])).difference(members) + ) + users = User.objects.filter(pk__in=users) + + serializer = UserLiteSerializer(users, many=True) + return Response( + { + "error": f"{len(users)} of the member(s) are not a part of the workspace", + "members": serializer.data, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + + serializer = TeamSerializer( + data=request.data, context={"workspace": workspace} + ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/app/views/workspace/module.py b/apiserver/plane/app/views/workspace/module.py new file mode 100644 index 000000000..fbd760271 --- /dev/null +++ b/apiserver/plane/app/views/workspace/module.py @@ -0,0 +1,104 @@ +# Django imports +from django.db.models import ( + Prefetch, + Q, + Count, +) + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.views.base import BaseAPIView +from plane.db.models import ( + Module, + ModuleLink, +) +from plane.app.permissions import WorkspaceViewerPermission +from plane.app.serializers.module import ModuleSerializer + +class WorkspaceModulesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def get(self, request, slug): + modules = ( + Module.objects.filter(workspace__slug=slug) + .select_related("project") + .select_related("workspace") + .select_related("lead") + .prefetch_related("members") + .prefetch_related( + Prefetch( + "link_module", + queryset=ModuleLink.objects.select_related( + "module", "created_by" + ), + ) + ) + .annotate( + total_issues=Count( + "issue_module", + filter=Q( + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ), + ) + .annotate( + completed_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="completed", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="cancelled", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="started", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="unstarted", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_module__issue__state__group", + filter=Q( + issue_module__issue__state__group="backlog", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ) + ) + .order_by(self.kwargs.get("order_by", "-created_at")) + ) + + serializer = ModuleSerializer(modules, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/state.py b/apiserver/plane/app/views/workspace/state.py new file mode 100644 index 000000000..d44f83e73 --- /dev/null +++ b/apiserver/plane/app/views/workspace/state.py @@ -0,0 +1,25 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import StateSerializer +from plane.app.views.base import BaseAPIView +from plane.db.models import State +from plane.app.permissions import WorkspaceEntityPermission +from plane.utils.cache import cache_response + +class WorkspaceStatesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + @cache_response(60 * 60 * 2) + def get(self, request, slug): + states = State.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + serializer = StateSerializer(states, many=True).data + return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/user.py b/apiserver/plane/app/views/workspace/user.py new file mode 100644 index 000000000..36b00b738 --- /dev/null +++ b/apiserver/plane/app/views/workspace/user.py @@ -0,0 +1,573 @@ +# Python imports +from datetime import date +from dateutil.relativedelta import relativedelta + +# Django imports +from django.utils import timezone +from django.db.models import ( + OuterRef, + Func, + F, + Q, + Count, + Case, + Value, + CharField, + When, + Max, + IntegerField, + UUIDField, +) +from django.db.models.functions import ExtractWeek, Cast +from django.db.models.fields import DateField +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.serializers import ( + WorkSpaceSerializer, + ProjectMemberSerializer, + IssueActivitySerializer, + IssueSerializer, + WorkspaceUserPropertiesSerializer, +) +from plane.app.views.base import BaseAPIView +from plane.db.models import ( + User, + Workspace, + ProjectMember, + IssueActivity, + Issue, + IssueLink, + IssueAttachment, + IssueSubscriber, + Project, + WorkspaceMember, + CycleIssue, + WorkspaceUserProperties, +) +from plane.app.permissions import ( + WorkspaceEntityPermission, + WorkspaceViewerPermission, +) +from plane.utils.issue_filters import issue_filters + + +class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): + def get(self, request): + user = User.objects.get(pk=request.user.id) + + last_workspace_id = user.last_workspace_id + + if last_workspace_id is None: + return Response( + { + "project_details": [], + "workspace_details": {}, + }, + status=status.HTTP_200_OK, + ) + + workspace = Workspace.objects.get(pk=last_workspace_id) + workspace_serializer = WorkSpaceSerializer(workspace) + + project_member = ProjectMember.objects.filter( + workspace_id=last_workspace_id, member=request.user + ).select_related("workspace", "project", "member", "workspace__owner") + + project_member_serializer = ProjectMemberSerializer( + project_member, many=True + ) + + return Response( + { + "workspace_details": workspace_serializer.data, + "project_details": project_member_serializer.data, + }, + status=status.HTTP_200_OK, + ) + + +class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def get(self, request, slug, user_id): + fields = [ + field + for field in request.GET.get("fields", "").split(",") + if field + ] + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + order_by_param = request.GET.get("order_by", "-created_at") + issue_queryset = ( + Issue.issue_objects.filter( + Q(assignees__in=[user_id]) + | Q(created_by_id=user_id) + | Q(issue_subscribers__subscriber_id=user_id), + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True + ) + .filter(**filters) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .order_by("created_at") + ).distinct() + + # 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 = IssueSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + return Response(issues, status=status.HTTP_200_OK) + + +class WorkspaceUserPropertiesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def patch(self, request, slug): + workspace_properties = WorkspaceUserProperties.objects.get( + user=request.user, + workspace__slug=slug, + ) + + workspace_properties.filters = request.data.get( + "filters", workspace_properties.filters + ) + workspace_properties.display_filters = request.data.get( + "display_filters", workspace_properties.display_filters + ) + workspace_properties.display_properties = request.data.get( + "display_properties", workspace_properties.display_properties + ) + workspace_properties.save() + + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug): + ( + workspace_properties, + _, + ) = WorkspaceUserProperties.objects.get_or_create( + user=request.user, workspace__slug=slug + ) + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class WorkspaceUserProfileEndpoint(BaseAPIView): + def get(self, request, slug, user_id): + user_data = User.objects.get(pk=user_id) + + requesting_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + projects = [] + if requesting_workspace_member.role >= 10: + projects = ( + Project.objects.filter( + workspace__slug=slug, + project_projectmember__member=request.user, + project_projectmember__is_active=True, + ) + .annotate( + created_issues=Count( + "project_issue", + filter=Q( + project_issue__created_by_id=user_id, + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .annotate( + assigned_issues=Count( + "project_issue", + filter=Q( + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "project_issue", + filter=Q( + project_issue__completed_at__isnull=False, + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "project_issue", + filter=Q( + project_issue__state__group__in=[ + "backlog", + "unstarted", + "started", + ], + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), + ) + ) + .values( + "id", + "logo_props", + "created_issues", + "assigned_issues", + "completed_issues", + "pending_issues", + ) + ) + + return Response( + { + "project_data": projects, + "user_data": { + "email": user_data.email, + "first_name": user_data.first_name, + "last_name": user_data.last_name, + "avatar": user_data.avatar, + "cover_image": user_data.cover_image, + "date_joined": user_data.date_joined, + "user_timezone": user_data.user_timezone, + "display_name": user_data.display_name, + }, + }, + status=status.HTTP_200_OK, + ) + + +class WorkspaceUserActivityEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug, user_id): + projects = request.query_params.getlist("project", []) + + queryset = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + actor=user_id, + ).select_related("actor", "workspace", "issue", "project") + + if projects: + queryset = queryset.filter(project__in=projects) + + return self.paginate( + request=request, + queryset=queryset, + on_results=lambda issue_activities: IssueActivitySerializer( + issue_activities, many=True + ).data, + ) + + +class WorkspaceUserProfileStatsEndpoint(BaseAPIView): + def get(self, request, slug, user_id): + filters = issue_filters(request.query_params, "GET") + + state_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .annotate(state_group=F("state__group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) + + priority_order = ["urgent", "high", "medium", "low", "none"] + + priority_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .values("priority") + .annotate(priority_count=Count("priority")) + .filter(priority_count__gte=1) + .annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + default=Value(len(priority_order)), + output_field=IntegerField(), + ) + ) + .order_by("priority_order") + ) + + created_issues = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + created_by_id=user_id, + ) + .filter(**filters) + .count() + ) + + assigned_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + pending_issues_count = ( + Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + completed_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + state__group="completed", + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + subscribed_issues_count = ( + IssueSubscriber.objects.filter( + workspace__slug=slug, + subscriber_id=user_id, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + .filter(**filters) + .count() + ) + + upcoming_cycles = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__gt=timezone.now().date(), + issue__assignees__in=[ + user_id, + ], + ).values("cycle__name", "cycle__id", "cycle__project_id") + + present_cycle = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__lt=timezone.now().date(), + cycle__end_date__gt=timezone.now().date(), + issue__assignees__in=[ + user_id, + ], + ).values("cycle__name", "cycle__id", "cycle__project_id") + + return Response( + { + "state_distribution": state_distribution, + "priority_distribution": priority_distribution, + "created_issues": created_issues, + "assigned_issues": assigned_issues_count, + "completed_issues": completed_issues_count, + "pending_issues": pending_issues_count, + "subscribed_issues": subscribed_issues_count, + "present_cycles": present_cycle, + "upcoming_cycles": upcoming_cycles, + } + ) + + +class UserActivityGraphEndpoint(BaseAPIView): + def get(self, request, slug): + issue_activities = ( + IssueActivity.objects.filter( + actor=request.user, + workspace__slug=slug, + created_at__date__gte=date.today() + relativedelta(months=-6), + ) + .annotate(created_date=Cast("created_at", DateField())) + .values("created_date") + .annotate(activity_count=Count("created_date")) + .order_by("created_date") + ) + + return Response(issue_activities, status=status.HTTP_200_OK) + + +class UserIssueCompletedGraphEndpoint(BaseAPIView): + def get(self, request, slug): + month = request.GET.get("month", 1) + + issues = ( + Issue.issue_objects.filter( + assignees__in=[request.user], + workspace__slug=slug, + completed_at__month=month, + completed_at__isnull=False, + ) + .annotate(completed_week=ExtractWeek("completed_at")) + .annotate(week=F("completed_week") % 4) + .values("week") + .annotate(completed_count=Count("completed_week")) + .order_by("week") + ) + + return Response(issues, status=status.HTTP_200_OK) diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index 778956229..62620ab9d 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -1,8 +1,6 @@ # Python imports import csv import io -import requests -import json # Django imports from django.core.mail import EmailMultiAlternatives, get_connection diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index 2a98c6b33..c3e6e214a 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -17,17 +17,20 @@ from plane.db.models import EmailNotificationLog, User, Issue from plane.license.utils.instance_value import get_email_configuration from plane.settings.redis import redis_instance + # acquire and delete redis lock def acquire_lock(lock_id, expire_time=300): redis_client = redis_instance() """Attempt to acquire a lock with a specified expiration time.""" - return redis_client.set(lock_id, 'true', nx=True, ex=expire_time) + return redis_client.set(lock_id, "true", nx=True, ex=expire_time) + def release_lock(lock_id): """Release a lock.""" redis_client = redis_instance() redis_client.delete(lock_id) + @shared_task def stack_email_notification(): # get all email notifications @@ -66,9 +69,7 @@ def stack_email_notification(): receiver_notification.get("entity_identifier"), {} ).setdefault( str(receiver_notification.get("triggered_by_id")), [] - ).append( - receiver_notification.get("data") - ) + ).append(receiver_notification.get("data")) # append processed notifications processed_notifications.append(receiver_notification.get("id")) email_notification_ids.append(receiver_notification.get("id")) @@ -101,31 +102,31 @@ def create_payload(notification_data): # Append old_value if it's not empty and not already in the list if old_value: - data.setdefault(actor_id, {}).setdefault( - field, {} - ).setdefault("old_value", []).append( - old_value - ) if old_value not in data.setdefault( - actor_id, {} - ).setdefault( - field, {} - ).get( - "old_value", [] - ) else None + ( + data.setdefault(actor_id, {}) + .setdefault(field, {}) + .setdefault("old_value", []) + .append(old_value) + if old_value + not in data.setdefault(actor_id, {}) + .setdefault(field, {}) + .get("old_value", []) + else None + ) # Append new_value if it's not empty and not already in the list if new_value: - data.setdefault(actor_id, {}).setdefault( - field, {} - ).setdefault("new_value", []).append( - new_value - ) if new_value not in data.setdefault( - actor_id, {} - ).setdefault( - field, {} - ).get( - "new_value", [] - ) else None + ( + data.setdefault(actor_id, {}) + .setdefault(field, {}) + .setdefault("new_value", []) + .append(new_value) + if new_value + not in data.setdefault(actor_id, {}) + .setdefault(field, {}) + .get("new_value", []) + else None + ) if not data.get("actor_id", {}).get("activity_time", False): data[actor_id]["activity_time"] = str( @@ -136,22 +137,24 @@ def create_payload(notification_data): return data + def process_mention(mention_component): - soup = BeautifulSoup(mention_component, 'html.parser') - mentions = soup.find_all('mention-component') + soup = BeautifulSoup(mention_component, "html.parser") + mentions = soup.find_all("mention-component") for mention in mentions: - user_id = mention['id'] + user_id = mention["id"] user = User.objects.get(pk=user_id) user_name = user.display_name highlighted_name = f"@{user_name}" mention.replace_with(highlighted_name) return str(soup) + def process_html_content(content): processed_content_list = [] for html_content in content: processed_content = process_mention(html_content) - processed_content_list.append(processed_content) + processed_content_list.append(processed_content) return processed_content_list @@ -169,7 +172,7 @@ def send_email_notification( if acquire_lock(lock_id=lock_id): # get the redis instance ri = redis_instance() - base_api = (ri.get(str(issue_id)).decode()) + base_api = ri.get(str(issue_id)).decode() data = create_payload(notification_data=notification_data) # Get email configurations @@ -206,8 +209,12 @@ def send_email_notification( } ) if mention: - mention["new_value"] = process_html_content(mention.get("new_value")) - mention["old_value"] = process_html_content(mention.get("old_value")) + mention["new_value"] = process_html_content( + mention.get("new_value") + ) + mention["old_value"] = process_html_content( + mention.get("old_value") + ) comments.append( { "actor_comments": mention, @@ -220,7 +227,9 @@ def send_email_notification( ) activity_time = changes.pop("activity_time") # Parse the input string into a datetime object - formatted_time = datetime.strptime(activity_time, "%Y-%m-%d %H:%M:%S").strftime("%H:%M %p") + formatted_time = datetime.strptime( + activity_time, "%Y-%m-%d %H:%M:%S" + ).strftime("%H:%M %p") if changes: template_data.append( @@ -237,12 +246,14 @@ def send_email_notification( }, "activity_time": str(formatted_time), } - ) + ) summary = "Updates were made to the issue by" # Send the mail - subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}" + subject = ( + f"{issue.project.identifier}-{issue.sequence_id} {issue.name}" + ) context = { "data": template_data, "summary": summary, @@ -257,7 +268,7 @@ def send_email_notification( }, "issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}", "project_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/", - "workspace":str(issue.project.workspace.slug), + "workspace": str(issue.project.workspace.slug), "project": str(issue.project.name), "user_preference": f"{base_api}/profile/preferences/email", "comments": comments, diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index d8522e769..f99e54215 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -144,12 +144,17 @@ def generate_table_row(issue): issue["description_stripped"], issue["state__name"], issue["priority"], - f"{issue['created_by__first_name']} {issue['created_by__last_name']}" - if issue["created_by__first_name"] and issue["created_by__last_name"] - else "", - f"{issue['assignees__first_name']} {issue['assignees__last_name']}" - if issue["assignees__first_name"] and issue["assignees__last_name"] - else "", + ( + f"{issue['created_by__first_name']} {issue['created_by__last_name']}" + if issue["created_by__first_name"] + and issue["created_by__last_name"] + else "" + ), + ( + f"{issue['assignees__first_name']} {issue['assignees__last_name']}" + if issue["assignees__first_name"] and issue["assignees__last_name"] + else "" + ), issue["labels__name"], issue["issue_cycle__cycle__name"], dateConverter(issue["issue_cycle__cycle__start_date"]), @@ -172,12 +177,17 @@ def generate_json_row(issue): "Description": issue["description_stripped"], "State": issue["state__name"], "Priority": issue["priority"], - "Created By": f"{issue['created_by__first_name']} {issue['created_by__last_name']}" - if issue["created_by__first_name"] and issue["created_by__last_name"] - else "", - "Assignee": f"{issue['assignees__first_name']} {issue['assignees__last_name']}" - if issue["assignees__first_name"] and issue["assignees__last_name"] - else "", + "Created By": ( + f"{issue['created_by__first_name']} {issue['created_by__last_name']}" + if issue["created_by__first_name"] + and issue["created_by__last_name"] + else "" + ), + "Assignee": ( + f"{issue['assignees__first_name']} {issue['assignees__last_name']}" + if issue["assignees__first_name"] and issue["assignees__last_name"] + else "" + ), "Labels": issue["labels__name"], "Cycle Name": issue["issue_cycle__cycle__name"], "Cycle Start Date": dateConverter( @@ -292,7 +302,7 @@ def issue_export_task( workspace__id=workspace_id, project_id__in=project_ids, project__project_projectmember__member=exporter_instance.initiated_by_id, - project__project_projectmember__is_active=True + project__project_projectmember__is_active=True, ) .select_related( "project", "workspace", "state", "parent", "created_by" diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index a2ac62927..1d3b68477 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -1,7 +1,4 @@ # Python import -import os -import requests -import json # Django imports from django.core.mail import EmailMultiAlternatives, get_connection @@ -20,9 +17,7 @@ from plane.license.utils.instance_value import get_email_configuration @shared_task def forgot_password(first_name, email, uidb64, token, current_site): try: - relative_link = ( - f"/accounts/reset-password/?uidb64={uidb64}&token={token}&email={email}" - ) + relative_link = f"/accounts/reset-password/?uidb64={uidb64}&token={token}&email={email}" abs_url = str(current_site) + relative_link ( diff --git a/apiserver/plane/bgtasks/importer_task.py b/apiserver/plane/bgtasks/importer_task.py deleted file mode 100644 index 7a1dc4fc6..000000000 --- a/apiserver/plane/bgtasks/importer_task.py +++ /dev/null @@ -1,201 +0,0 @@ -# Python imports -import json -import requests -import uuid - -# Django imports -from django.conf import settings -from django.core.serializers.json import DjangoJSONEncoder -from django.contrib.auth.hashers import make_password - -# Third Party imports -from celery import shared_task -from sentry_sdk import capture_exception - -# Module imports -from plane.app.serializers import ImporterSerializer -from plane.db.models import ( - Importer, - WorkspaceMember, - GithubRepositorySync, - GithubRepository, - ProjectMember, - WorkspaceIntegration, - Label, - User, - IssueProperty, - UserNotificationPreference, -) - - -@shared_task -def service_importer(service, importer_id): - try: - importer = Importer.objects.get(pk=importer_id) - importer.status = "processing" - importer.save() - - users = importer.data.get("users", []) - - # Check if we need to import users as well - if len(users): - # For all invited users create the users - new_users = User.objects.bulk_create( - [ - User( - email=user.get("email").strip().lower(), - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, - ) - for user in users - if user.get("import", False) == "invite" - ], - batch_size=100, - ignore_conflicts=True, - ) - - _ = UserNotificationPreference.objects.bulk_create( - [UserNotificationPreference(user=user) for user in new_users], - batch_size=100, - ) - - workspace_users = User.objects.filter( - email__in=[ - user.get("email").strip().lower() - for user in users - if user.get("import", False) == "invite" - or user.get("import", False) == "map" - ] - ) - - # Check if any of the users are already member of workspace - _ = WorkspaceMember.objects.filter( - member__in=[user for user in workspace_users], - workspace_id=importer.workspace_id, - ).update(is_active=True) - - # Add new users to Workspace and project automatically - WorkspaceMember.objects.bulk_create( - [ - WorkspaceMember( - member=user, - workspace_id=importer.workspace_id, - created_by=importer.created_by, - ) - for user in workspace_users - ], - batch_size=100, - ignore_conflicts=True, - ) - - ProjectMember.objects.bulk_create( - [ - ProjectMember( - project_id=importer.project_id, - workspace_id=importer.workspace_id, - member=user, - created_by=importer.created_by, - ) - for user in workspace_users - ], - batch_size=100, - ignore_conflicts=True, - ) - - IssueProperty.objects.bulk_create( - [ - IssueProperty( - project_id=importer.project_id, - workspace_id=importer.workspace_id, - user=user, - created_by=importer.created_by, - ) - for user in workspace_users - ], - batch_size=100, - ignore_conflicts=True, - ) - - # Check if sync config is on for github importers - if service == "github" and importer.config.get("sync", False): - name = importer.metadata.get("name", False) - url = importer.metadata.get("url", False) - config = importer.metadata.get("config", {}) - owner = importer.metadata.get("owner", False) - repository_id = importer.metadata.get("repository_id", False) - - workspace_integration = WorkspaceIntegration.objects.get( - workspace_id=importer.workspace_id, - integration__provider="github", - ) - - # Delete the old repository object - GithubRepositorySync.objects.filter( - project_id=importer.project_id - ).delete() - GithubRepository.objects.filter( - project_id=importer.project_id - ).delete() - - # Create a Label for github - label = Label.objects.filter( - name="GitHub", project_id=importer.project_id - ).first() - - if label is None: - label = Label.objects.create( - name="GitHub", - project_id=importer.project_id, - description="Label to sync Plane issues with GitHub issues", - color="#003773", - ) - # Create repository - repo = GithubRepository.objects.create( - name=name, - url=url, - config=config, - repository_id=repository_id, - owner=owner, - project_id=importer.project_id, - ) - - # Create repo sync - _ = GithubRepositorySync.objects.create( - repository=repo, - workspace_integration=workspace_integration, - actor=workspace_integration.actor, - credentials=importer.data.get("credentials", {}), - project_id=importer.project_id, - label=label, - ) - - # Add bot as a member in the project - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, - role=20, - project_id=importer.project_id, - ) - - if settings.PROXY_BASE_URL: - headers = {"Content-Type": "application/json"} - import_data_json = json.dumps( - ImporterSerializer(importer).data, - cls=DjangoJSONEncoder, - ) - _ = requests.post( - f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/", - json=import_data_json, - headers=headers, - ) - - return - except Exception as e: - importer = Importer.objects.get(pk=importer_id) - importer.status = "failed" - importer.save() - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) - capture_exception(e) - return diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 2a16ee911..6aa6b6695 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -53,7 +53,7 @@ def track_name( field="name", project_id=project_id, workspace_id=workspace_id, - comment=f"updated the name to", + comment="updated the name to", epoch=epoch, ) ) @@ -96,7 +96,7 @@ def track_description( field="description", project_id=project_id, workspace_id=workspace_id, - comment=f"updated the description to", + comment="updated the description to", epoch=epoch, ) ) @@ -130,22 +130,26 @@ def track_parent( issue_id=issue_id, actor_id=actor_id, verb="updated", - old_value=f"{old_parent.project.identifier}-{old_parent.sequence_id}" - if old_parent is not None - else "", - new_value=f"{new_parent.project.identifier}-{new_parent.sequence_id}" - if new_parent is not None - else "", + old_value=( + f"{old_parent.project.identifier}-{old_parent.sequence_id}" + if old_parent is not None + else "" + ), + new_value=( + f"{new_parent.project.identifier}-{new_parent.sequence_id}" + if new_parent is not None + else "" + ), field="parent", project_id=project_id, workspace_id=workspace_id, - comment=f"updated the parent issue to", - old_identifier=old_parent.id - if old_parent is not None - else None, - new_identifier=new_parent.id - if new_parent is not None - else None, + comment="updated the parent issue to", + old_identifier=( + old_parent.id if old_parent is not None else None + ), + new_identifier=( + new_parent.id if new_parent is not None else None + ), epoch=epoch, ) ) @@ -173,7 +177,7 @@ def track_priority( field="priority", project_id=project_id, workspace_id=workspace_id, - comment=f"updated the priority to", + comment="updated the priority to", epoch=epoch, ) ) @@ -206,7 +210,7 @@ def track_state( field="state", project_id=project_id, workspace_id=workspace_id, - comment=f"updated the state to", + comment="updated the state to", old_identifier=old_state.id, new_identifier=new_state.id, epoch=epoch, @@ -233,16 +237,20 @@ def track_target_date( issue_id=issue_id, actor_id=actor_id, verb="updated", - old_value=current_instance.get("target_date") - if current_instance.get("target_date") is not None - else "", - new_value=requested_data.get("target_date") - if requested_data.get("target_date") is not None - else "", + old_value=( + current_instance.get("target_date") + if current_instance.get("target_date") is not None + else "" + ), + new_value=( + requested_data.get("target_date") + if requested_data.get("target_date") is not None + else "" + ), field="target_date", project_id=project_id, workspace_id=workspace_id, - comment=f"updated the target date to", + comment="updated the target date to", epoch=epoch, ) ) @@ -265,16 +273,20 @@ def track_start_date( issue_id=issue_id, actor_id=actor_id, verb="updated", - old_value=current_instance.get("start_date") - if current_instance.get("start_date") is not None - else "", - new_value=requested_data.get("start_date") - if requested_data.get("start_date") is not None - else "", + old_value=( + current_instance.get("start_date") + if current_instance.get("start_date") is not None + else "" + ), + new_value=( + requested_data.get("start_date") + if requested_data.get("start_date") is not None + else "" + ), field="start_date", project_id=project_id, workspace_id=workspace_id, - comment=f"updated the start date to ", + comment="updated the start date to ", epoch=epoch, ) ) @@ -334,7 +346,7 @@ def track_labels( field="labels", project_id=project_id, workspace_id=workspace_id, - comment=f"removed label ", + comment="removed label ", old_identifier=label.id, new_identifier=None, epoch=epoch, @@ -364,7 +376,6 @@ def track_assignees( else set() ) - added_assignees = requested_assignees - current_assignees dropped_assginees = current_assignees - requested_assignees @@ -381,7 +392,7 @@ def track_assignees( field="assignees", project_id=project_id, workspace_id=workspace_id, - comment=f"added assignee ", + comment="added assignee ", new_identifier=assignee.id, epoch=epoch, ) @@ -414,7 +425,7 @@ def track_assignees( field="assignees", project_id=project_id, workspace_id=workspace_id, - comment=f"removed assignee ", + comment="removed assignee ", old_identifier=assignee.id, epoch=epoch, ) @@ -439,16 +450,20 @@ def track_estimate_points( issue_id=issue_id, actor_id=actor_id, verb="updated", - old_value=current_instance.get("estimate_point") - if current_instance.get("estimate_point") is not None - else "", - new_value=requested_data.get("estimate_point") - if requested_data.get("estimate_point") is not None - else "", + old_value=( + current_instance.get("estimate_point") + if current_instance.get("estimate_point") is not None + else "" + ), + new_value=( + requested_data.get("estimate_point") + if requested_data.get("estimate_point") is not None + else "" + ), field="estimate_point", project_id=project_id, workspace_id=workspace_id, - comment=f"updated the estimate point to ", + comment="updated the estimate point to ", epoch=epoch, ) ) @@ -529,7 +544,7 @@ def track_closed_to( field="state", project_id=project_id, workspace_id=workspace_id, - comment=f"Plane updated the state to ", + comment="Plane updated the state to ", old_identifier=None, new_identifier=updated_state.id, epoch=epoch, @@ -552,7 +567,7 @@ def create_issue_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"created the issue", + comment="created the issue", verb="created", actor_id=actor_id, epoch=epoch, @@ -635,7 +650,7 @@ def delete_issue_activity( IssueActivity( project_id=project_id, workspace_id=workspace_id, - comment=f"deleted the issue", + comment="deleted the issue", verb="deleted", actor_id=actor_id, field="issue", @@ -666,7 +681,7 @@ def create_comment_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"created a comment", + comment="created a comment", verb="created", actor_id=actor_id, field="comment", @@ -703,7 +718,7 @@ def update_comment_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"updated a comment", + comment="updated a comment", verb="updated", actor_id=actor_id, field="comment", @@ -732,7 +747,7 @@ def delete_comment_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"deleted the comment", + comment="deleted the comment", verb="deleted", actor_id=actor_id, field="comment", @@ -932,7 +947,11 @@ def delete_module_issue_activity( project_id=project_id, workspace_id=workspace_id, comment=f"removed this issue from {module_name}", - old_identifier=requested_data.get("module_id") if requested_data.get("module_id") is not None else None, + old_identifier=( + requested_data.get("module_id") + if requested_data.get("module_id") is not None + else None + ), epoch=epoch, ) ) @@ -960,7 +979,7 @@ def create_link_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"created a link", + comment="created a link", verb="created", actor_id=actor_id, field="link", @@ -994,7 +1013,7 @@ def update_link_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"updated a link", + comment="updated a link", verb="updated", actor_id=actor_id, field="link", @@ -1026,7 +1045,7 @@ def delete_link_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"deleted the link", + comment="deleted the link", verb="deleted", actor_id=actor_id, field="link", @@ -1059,7 +1078,7 @@ def create_attachment_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"created an attachment", + comment="created an attachment", verb="created", actor_id=actor_id, field="attachment", @@ -1085,7 +1104,7 @@ def delete_attachment_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"deleted the attachment", + comment="deleted the attachment", verb="deleted", actor_id=actor_id, field="attachment", @@ -1362,12 +1381,15 @@ def create_issue_relation_activity( verb="created", old_value="", new_value=f"{issue.project.identifier}-{issue.sequence_id}", - field="blocking" - if requested_data.get("relation_type") == "blocked_by" - else ( - "blocked_by" - if requested_data.get("relation_type") == "blocking" - else requested_data.get("relation_type") + field=( + "blocking" + if requested_data.get("relation_type") == "blocked_by" + else ( + "blocked_by" + if requested_data.get("relation_type") + == "blocking" + else requested_data.get("relation_type") + ) ), project_id=project_id, workspace_id=workspace_id, @@ -1418,12 +1440,14 @@ def delete_issue_relation_activity( verb="deleted", old_value=f"{issue.project.identifier}-{issue.sequence_id}", new_value="", - field="blocking" - if requested_data.get("relation_type") == "blocked_by" - else ( - "blocked_by" - if requested_data.get("relation_type") == "blocking" - else requested_data.get("relation_type") + field=( + "blocking" + if requested_data.get("relation_type") == "blocked_by" + else ( + "blocked_by" + if requested_data.get("relation_type") == "blocking" + else requested_data.get("relation_type") + ) ), project_id=project_id, workspace_id=workspace_id, @@ -1449,7 +1473,7 @@ def create_draft_issue_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"drafted the issue", + comment="drafted the issue", field="draft", verb="created", actor_id=actor_id, @@ -1476,14 +1500,14 @@ def update_draft_issue_activity( ) if ( requested_data.get("is_draft") is not None - and requested_data.get("is_draft") == False + and requested_data.get("is_draft") is False ): issue_activities.append( IssueActivity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"created the issue", + comment="created the issue", verb="updated", actor_id=actor_id, epoch=epoch, @@ -1495,7 +1519,7 @@ def update_draft_issue_activity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment=f"updated the draft issue", + comment="updated the draft issue", field="draft", verb="updated", actor_id=actor_id, @@ -1518,7 +1542,7 @@ def delete_draft_issue_activity( IssueActivity( project_id=project_id, workspace_id=workspace_id, - comment=f"deleted the draft issue", + comment="deleted the draft issue", field="draft", verb="deleted", actor_id=actor_id, @@ -1557,7 +1581,7 @@ def issue_activity( try: issue.updated_at = timezone.now() issue.save(update_fields=["updated_at"]) - except Exception as e: + except Exception: pass ACTIVITY_MAPPER = { diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index c6c4d7515..08c07b7b3 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -79,7 +79,10 @@ def archive_old_issues(): issue_activity.delay( type="issue.activity.updated", requested_data=json.dumps( - {"archived_at": str(archive_at), "automation": True} + { + "archived_at": str(archive_at), + "automation": True, + } ), actor_id=str(project.created_by_id), issue_id=issue.id, diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index b94ec4bfe..019f5b13c 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -1,7 +1,4 @@ # Python imports -import os -import requests -import json # Django imports from django.core.mail import EmailMultiAlternatives, get_connection diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index 0a843e4a6..5725abc62 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -40,7 +40,9 @@ def update_mentions_for_issue(issue, project, new_mentions, removed_mention): ) IssueMention.objects.bulk_create(aggregated_issue_mentions, batch_size=100) - IssueMention.objects.filter(issue=issue, mention__in=removed_mention).delete() + IssueMention.objects.filter( + issue=issue, mention__in=removed_mention + ).delete() def get_new_mentions(requested_instance, current_instance): @@ -92,7 +94,9 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions): project_id=project_id, ).exists() and not IssueAssignee.objects.filter( - project_id=project_id, issue_id=issue_id, assignee_id=mention_id + project_id=project_id, + issue_id=issue_id, + assignee_id=mention_id, ).exists() and not Issue.objects.filter( project_id=project_id, pk=issue_id, created_by_id=mention_id @@ -120,12 +124,14 @@ def extract_mentions(issue_instance): data = json.loads(issue_instance) html = data.get("description_html") soup = BeautifulSoup(html, "html.parser") - mention_tags = soup.find_all("mention-component", attrs={"target": "users"}) + mention_tags = soup.find_all( + "mention-component", attrs={"target": "users"} + ) mentions = [mention_tag["id"] for mention_tag in mention_tags] return list(set(mentions)) - except Exception as e: + except Exception: return [] @@ -134,11 +140,13 @@ def extract_comment_mentions(comment_value): try: mentions = [] soup = BeautifulSoup(comment_value, "html.parser") - mentions_tags = soup.find_all("mention-component", attrs={"target": "users"}) + mentions_tags = soup.find_all( + "mention-component", attrs={"target": "users"} + ) for mention_tag in mentions_tags: mentions.append(mention_tag["id"]) return list(set(mentions)) - except Exception as e: + except Exception: return [] @@ -157,7 +165,13 @@ def get_new_comment_mentions(new_value, old_value): def create_mention_notification( - project, notification_comment, issue, actor_id, mention_id, issue_id, activity + project, + notification_comment, + issue, + actor_id, + mention_id, + issue_id, + activity, ): return Notification( workspace=project.workspace, @@ -304,9 +318,11 @@ def notifications( # add the user to issue subscriber try: _ = IssueSubscriber.objects.get_or_create( - project_id=project_id, issue_id=issue_id, subscriber_id=actor_id + project_id=project_id, + issue_id=issue_id, + subscriber_id=actor_id, ) - except Exception as e: + except Exception: pass project = Project.objects.get(pk=project_id) @@ -334,11 +350,14 @@ def notifications( user_id=subscriber ) - for issue_activity in issue_activities_created: + for issue_activity in issue_activities_created: # If activity done in blocking then blocked by email should not go - if issue_activity.get("issue_detail").get("id") != issue_id: - continue; - + if ( + issue_activity.get("issue_detail").get("id") + != issue_id + ): + continue + # Do not send notification for description update if issue_activity.get("field") == "description": continue @@ -471,7 +490,9 @@ def notifications( if issue_comment is not None else "" ), - "activity_time": issue_activity.get("created_at"), + "activity_time": issue_activity.get( + "created_at" + ), }, }, ) @@ -552,7 +573,9 @@ def notifications( "old_value": str( issue_activity.get("old_value") ), - "activity_time": issue_activity.get("created_at"), + "activity_time": issue_activity.get( + "created_at" + ), }, }, ) @@ -640,7 +663,9 @@ def notifications( "old_value": str( last_activity.old_value ), - "activity_time": issue_activity.get("created_at"), + "activity_time": issue_activity.get( + "created_at" + ), }, }, ) @@ -697,7 +722,9 @@ def notifications( "old_value" ) ), - "activity_time": issue_activity.get("created_at"), + "activity_time": issue_activity.get( + "created_at" + ), }, }, ) diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index a986de332..d24db5ae9 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -1,5 +1,4 @@ # Python import -import os # Django imports from django.core.mail import EmailMultiAlternatives, get_connection @@ -75,7 +74,7 @@ def project_invitation(email, project_id, token, current_site, invitor): msg.attach_alternative(html_content, "text/html") msg.send() return - except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e: + except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist): return except Exception as e: # Print logs if in DEBUG mode diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index 605f48dd9..358fd7a85 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -159,7 +159,7 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site): ) # Retry logic if self.request.retries >= self.max_retries: - Webhook.objects.filter(pk=webhook.id).update(is_active=False) + Webhook.objects.filter(pk=webhook.id).update(is_active=False) if webhook: # send email for the deactivation of the webhook send_webhook_deactivation_email( @@ -215,9 +215,11 @@ def send_webhook(event, payload, kw, action, slug, bulk, current_site): event_data = [ get_model_data( event=event, - event_id=payload.get("id") - if isinstance(payload, dict) - else None, + event_id=( + payload.get("id") + if isinstance(payload, dict) + else None + ), many=False, ) ] @@ -244,7 +246,9 @@ def send_webhook(event, payload, kw, action, slug, bulk, current_site): @shared_task -def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reason): +def send_webhook_deactivation_email( + webhook_id, receiver_id, current_site, reason +): # Get email configurations ( EMAIL_HOST, @@ -256,15 +260,17 @@ def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reaso ) = get_email_configuration() receiver = User.objects.get(pk=receiver_id) - webhook = Webhook.objects.get(pk=webhook_id) - subject="Webhook Deactivated" - message=f"Webhook {webhook.url} has been deactivated due to failed requests." + webhook = Webhook.objects.get(pk=webhook_id) + subject = "Webhook Deactivated" + message = ( + f"Webhook {webhook.url} has been deactivated due to failed requests." + ) # Send the mail context = { "email": receiver.email, "message": message, - "webhook_url":f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}", + "webhook_url": f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}", } html_content = render_to_string( "emails/notifications/webhook-deactivate.html", context diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index 06dd6e8cd..cc3000bbb 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -1,7 +1,4 @@ # Python imports -import os -import requests -import json # Django imports from django.core.mail import EmailMultiAlternatives, get_connection @@ -12,8 +9,6 @@ from django.conf import settings # Third party imports from celery import shared_task from sentry_sdk import capture_exception -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError # Module imports from plane.db.models import Workspace, WorkspaceMemberInvite, User @@ -83,7 +78,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): msg.send() return - except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist) as e: + except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist): print("Workspace or WorkspaceMember Invite Does not exists") return except Exception as e: diff --git a/apiserver/plane/celery.py b/apiserver/plane/celery.py index 0912e276a..056dfb16b 100644 --- a/apiserver/plane/celery.py +++ b/apiserver/plane/celery.py @@ -2,7 +2,6 @@ import os from celery import Celery from plane.settings.redis import redis_instance from celery.schedules import crontab -from django.utils.timezone import timedelta # Set the default Django settings module for the 'celery' program. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") @@ -31,7 +30,7 @@ app.conf.beat_schedule = { }, "check-every-five-minutes-to-send-email-notifications": { "task": "plane.bgtasks.email_notification_task.stack_email_notification", - "schedule": crontab(minute='*/5') + "schedule": crontab(minute="*/5"), }, } diff --git a/apiserver/plane/db/management/commands/reset_password.py b/apiserver/plane/db/management/commands/reset_password.py index d48c24b1c..bca6c3560 100644 --- a/apiserver/plane/db/management/commands/reset_password.py +++ b/apiserver/plane/db/management/commands/reset_password.py @@ -52,5 +52,5 @@ class Command(BaseCommand): user.save() self.stdout.write( - self.style.SUCCESS(f"User password updated succesfully") + self.style.SUCCESS("User password updated succesfully") ) diff --git a/apiserver/plane/db/management/commands/wait_for_migrations.py b/apiserver/plane/db/management/commands/wait_for_migrations.py index 51f2cf339..91c8a4ce8 100644 --- a/apiserver/plane/db/management/commands/wait_for_migrations.py +++ b/apiserver/plane/db/management/commands/wait_for_migrations.py @@ -4,15 +4,18 @@ from django.core.management.base import BaseCommand from django.db.migrations.executor import MigrationExecutor from django.db import connections, DEFAULT_DB_ALIAS + class Command(BaseCommand): - help = 'Wait for database migrations to complete before starting Celery worker/beat' + help = "Wait for database migrations to complete before starting Celery worker/beat" def handle(self, *args, **kwargs): while self._pending_migrations(): self.stdout.write("Waiting for database migrations to complete...") time.sleep(10) # wait for 10 seconds before checking again - self.stdout.write(self.style.SUCCESS("No migrations Pending. Starting processes ...")) + self.stdout.write( + self.style.SUCCESS("No migrations Pending. Starting processes ...") + ) def _pending_migrations(self): connection = connections[DEFAULT_DB_ALIAS] diff --git a/apiserver/plane/db/migrations/0038_auto_20230720_1505.py b/apiserver/plane/db/migrations/0038_auto_20230720_1505.py index 53e50ed41..5f11d9ade 100644 --- a/apiserver/plane/db/migrations/0038_auto_20230720_1505.py +++ b/apiserver/plane/db/migrations/0038_auto_20230720_1505.py @@ -1,6 +1,6 @@ # Generated by Django 4.2.3 on 2023-07-20 09:35 -from django.db import migrations, models +from django.db import migrations def restructure_theming(apps, schema_editor): diff --git a/apiserver/plane/db/migrations/0054_dashboard_widget_dashboardwidget.py b/apiserver/plane/db/migrations/0054_dashboard_widget_dashboardwidget.py index 933c229a1..b3b5cc8c1 100644 --- a/apiserver/plane/db/migrations/0054_dashboard_widget_dashboardwidget.py +++ b/apiserver/plane/db/migrations/0054_dashboard_widget_dashboardwidget.py @@ -7,71 +7,204 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('db', '0053_auto_20240102_1315'), + ("db", "0053_auto_20240102_1315"), ] operations = [ migrations.CreateModel( - name='Dashboard', + name="Dashboard", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=255)), - ('description_html', models.TextField(blank=True, default='

')), - ('identifier', models.UUIDField(null=True)), - ('is_default', models.BooleanField(default=False)), - ('type_identifier', models.CharField(choices=[('workspace', 'Workspace'), ('project', 'Project'), ('home', 'Home'), ('team', 'Team'), ('user', 'User')], default='home', max_length=30, verbose_name='Dashboard Type')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dashboards', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ( + "description_html", + models.TextField(blank=True, default="

"), + ), + ("identifier", models.UUIDField(null=True)), + ("is_default", models.BooleanField(default=False)), + ( + "type_identifier", + models.CharField( + choices=[ + ("workspace", "Workspace"), + ("project", "Project"), + ("home", "Home"), + ("team", "Team"), + ("user", "User"), + ], + default="home", + max_length=30, + verbose_name="Dashboard Type", + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "owned_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="dashboards", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), ], options={ - 'verbose_name': 'Dashboard', - 'verbose_name_plural': 'Dashboards', - 'db_table': 'dashboards', - 'ordering': ('-created_at',), + "verbose_name": "Dashboard", + "verbose_name_plural": "Dashboards", + "db_table": "dashboards", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='Widget', + name="Widget", fields=[ - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('key', models.CharField(max_length=255)), - ('filters', models.JSONField(default=dict)), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ("key", models.CharField(max_length=255)), + ("filters", models.JSONField(default=dict)), ], options={ - 'verbose_name': 'Widget', - 'verbose_name_plural': 'Widgets', - 'db_table': 'widgets', - 'ordering': ('-created_at',), + "verbose_name": "Widget", + "verbose_name_plural": "Widgets", + "db_table": "widgets", + "ordering": ("-created_at",), }, ), migrations.CreateModel( - name='DashboardWidget', + name="DashboardWidget", fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('is_visible', models.BooleanField(default=True)), - ('sort_order', models.FloatField(default=65535)), - ('filters', models.JSONField(default=dict)), - ('properties', models.JSONField(default=dict)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('dashboard', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dashboard_widgets', to='db.dashboard')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('widget', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dashboard_widgets', to='db.widget')), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("is_visible", models.BooleanField(default=True)), + ("sort_order", models.FloatField(default=65535)), + ("filters", models.JSONField(default=dict)), + ("properties", models.JSONField(default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "dashboard", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="dashboard_widgets", + to="db.dashboard", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "widget", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="dashboard_widgets", + to="db.widget", + ), + ), ], options={ - 'verbose_name': 'Dashboard Widget', - 'verbose_name_plural': 'Dashboard Widgets', - 'db_table': 'dashboard_widgets', - 'ordering': ('-created_at',), - 'unique_together': {('widget', 'dashboard')}, + "verbose_name": "Dashboard Widget", + "verbose_name_plural": "Dashboard Widgets", + "db_table": "dashboard_widgets", + "ordering": ("-created_at",), + "unique_together": {("widget", "dashboard")}, }, ), ] diff --git a/apiserver/plane/db/migrations/0055_auto_20240108_0648.py b/apiserver/plane/db/migrations/0055_auto_20240108_0648.py index e369c185d..b13fcdea1 100644 --- a/apiserver/plane/db/migrations/0055_auto_20240108_0648.py +++ b/apiserver/plane/db/migrations/0055_auto_20240108_0648.py @@ -62,7 +62,7 @@ def create_dashboards(apps, schema_editor): type_identifier="home", is_default=True, ) - for user_id in User.objects.values_list('id', flat=True) + for user_id in User.objects.values_list("id", flat=True) ], batch_size=2000, ) @@ -78,11 +78,13 @@ def create_dashboard_widgets(apps, schema_editor): widget_id=widget_id, dashboard_id=dashboard_id, ) - for widget_id in Widget.objects.values_list('id', flat=True) - for dashboard_id in Dashboard.objects.values_list('id', flat=True) + for widget_id in Widget.objects.values_list("id", flat=True) + for dashboard_id in Dashboard.objects.values_list("id", flat=True) ] - DashboardWidget.objects.bulk_create(updated_dashboard_widget, batch_size=2000) + DashboardWidget.objects.bulk_create( + updated_dashboard_widget, batch_size=2000 + ) class Migration(migrations.Migration): diff --git a/apiserver/plane/db/migrations/0057_auto_20240122_0901.py b/apiserver/plane/db/migrations/0057_auto_20240122_0901.py index 9204d43b3..a143917d2 100644 --- a/apiserver/plane/db/migrations/0057_auto_20240122_0901.py +++ b/apiserver/plane/db/migrations/0057_auto_20240122_0901.py @@ -2,12 +2,17 @@ from django.db import migrations + def create_notification_preferences(apps, schema_editor): - UserNotificationPreference = apps.get_model("db", "UserNotificationPreference") + UserNotificationPreference = apps.get_model( + "db", "UserNotificationPreference" + ) User = apps.get_model("db", "User") bulk_notification_preferences = [] - for user_id in User.objects.filter(is_bot=False).values_list("id", flat=True): + for user_id in User.objects.filter(is_bot=False).values_list( + "id", flat=True + ): bulk_notification_preferences.append( UserNotificationPreference( user_id=user_id, @@ -18,11 +23,10 @@ def create_notification_preferences(apps, schema_editor): bulk_notification_preferences, batch_size=1000, ignore_conflicts=True ) + class Migration(migrations.Migration): dependencies = [ ("db", "0056_usernotificationpreference_emailnotificationlog"), ] - operations = [ - migrations.RunPython(create_notification_preferences) - ] + operations = [migrations.RunPython(create_notification_preferences)] diff --git a/apiserver/plane/db/migrations/0058_alter_moduleissue_issue_and_more.py b/apiserver/plane/db/migrations/0058_alter_moduleissue_issue_and_more.py index 6238ef825..411cd47bd 100644 --- a/apiserver/plane/db/migrations/0058_alter_moduleissue_issue_and_more.py +++ b/apiserver/plane/db/migrations/0058_alter_moduleissue_issue_and_more.py @@ -5,19 +5,22 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('db', '0057_auto_20240122_0901'), + ("db", "0057_auto_20240122_0901"), ] operations = [ migrations.AlterField( - model_name='moduleissue', - name='issue', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_module', to='db.issue'), + model_name="moduleissue", + name="issue", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issue_module", + to="db.issue", + ), ), migrations.AlterUniqueTogether( - name='moduleissue', - unique_together={('issue', 'module')}, + name="moduleissue", + unique_together={("issue", "module")}, ), ] diff --git a/apiserver/plane/db/migrations/0059_auto_20240208_0957.py b/apiserver/plane/db/migrations/0059_auto_20240208_0957.py index c4c43fa4b..30d816a93 100644 --- a/apiserver/plane/db/migrations/0059_auto_20240208_0957.py +++ b/apiserver/plane/db/migrations/0059_auto_20240208_0957.py @@ -24,10 +24,9 @@ def widgets_filter_change(apps, schema_editor): # Bulk update the widgets Widget.objects.bulk_update(widgets_to_update, ["filters"], batch_size=10) + class Migration(migrations.Migration): dependencies = [ - ('db', '0058_alter_moduleissue_issue_and_more'), - ] - operations = [ - migrations.RunPython(widgets_filter_change) + ("db", "0058_alter_moduleissue_issue_and_more"), ] + operations = [migrations.RunPython(widgets_filter_change)] diff --git a/apiserver/plane/db/migrations/0060_cycle_progress_snapshot.py b/apiserver/plane/db/migrations/0060_cycle_progress_snapshot.py index 074e20a16..575836a35 100644 --- a/apiserver/plane/db/migrations/0060_cycle_progress_snapshot.py +++ b/apiserver/plane/db/migrations/0060_cycle_progress_snapshot.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('db', '0059_auto_20240208_0957'), + ("db", "0059_auto_20240208_0957"), ] operations = [ migrations.AddField( - model_name='cycle', - name='progress_snapshot', + model_name="cycle", + name="progress_snapshot", field=models.JSONField(default=dict), ), ] diff --git a/apiserver/plane/db/migrations/0061_project_logo_props.py b/apiserver/plane/db/migrations/0061_project_logo_props.py new file mode 100644 index 000000000..d8752d9dd --- /dev/null +++ b/apiserver/plane/db/migrations/0061_project_logo_props.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.7 on 2024-03-03 16:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + def update_project_logo_props(apps, schema_editor): + Project = apps.get_model("db", "Project") + + bulk_update_project_logo = [] + # Iterate through projects and update logo_props + for project in Project.objects.all(): + project.logo_props["in_use"] = "emoji" if project.emoji else "icon" + project.logo_props["emoji"] = { + "value": project.emoji if project.emoji else "", + "url": "", + } + project.logo_props["icon"] = { + "name": ( + project.icon_prop.get("name", "") + if project.icon_prop + else "" + ), + "color": ( + project.icon_prop.get("color", "") + if project.icon_prop + else "" + ), + } + bulk_update_project_logo.append(project) + + # Bulk update logo_props for all projects + Project.objects.bulk_update( + bulk_update_project_logo, ["logo_props"], batch_size=1000 + ) + + dependencies = [ + ("db", "0060_cycle_progress_snapshot"), + ] + + operations = [ + migrations.AlterField( + model_name="issuelink", + name="url", + field=models.TextField(), + ), + migrations.AddField( + model_name="project", + name="logo_props", + field=models.JSONField(default=dict), + ), + migrations.RunPython(update_project_logo_props), + ] diff --git a/apiserver/plane/db/mixins.py b/apiserver/plane/db/mixins.py index 263f9ab9a..f1756e5ad 100644 --- a/apiserver/plane/db/mixins.py +++ b/apiserver/plane/db/mixins.py @@ -1,12 +1,10 @@ # Python imports -import uuid # Django imports from django.db import models class TimeAuditModel(models.Model): - """To path when the record was created and last modified""" created_at = models.DateTimeField( @@ -22,7 +20,6 @@ class TimeAuditModel(models.Model): class UserAuditModel(models.Model): - """To path when the record was created and last modified""" created_by = models.ForeignKey( @@ -45,7 +42,6 @@ class UserAuditModel(models.Model): class AuditModel(TimeAuditModel, UserAuditModel): - """To path when the record was created and last modified""" class Meta: diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index d9096bd01..daa793c37 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -85,10 +85,14 @@ from .inbox import Inbox, InboxIssue from .analytic import AnalyticView -from .notification import Notification, UserNotificationPreference, EmailNotificationLog +from .notification import ( + Notification, + UserNotificationPreference, + EmailNotificationLog, +) from .exporter import ExporterHistory from .webhook import Webhook, WebhookLog -from .dashboard import Dashboard, DashboardWidget, Widget \ No newline at end of file +from .dashboard import Dashboard, DashboardWidget, Widget diff --git a/apiserver/plane/db/models/analytic.py b/apiserver/plane/db/models/analytic.py index d097051af..68747e8c4 100644 --- a/apiserver/plane/db/models/analytic.py +++ b/apiserver/plane/db/models/analytic.py @@ -1,6 +1,5 @@ # Django models from django.db import models -from django.conf import settings from .base import BaseModel diff --git a/apiserver/plane/db/models/dashboard.py b/apiserver/plane/db/models/dashboard.py index 05c5a893f..d07a70728 100644 --- a/apiserver/plane/db/models/dashboard.py +++ b/apiserver/plane/db/models/dashboard.py @@ -2,12 +2,12 @@ import uuid # Django imports from django.db import models -from django.conf import settings # Module imports from . import BaseModel from ..mixins import TimeAuditModel + class Dashboard(BaseModel): DASHBOARD_CHOICES = ( ("workspace", "Workspace"), @@ -45,7 +45,11 @@ class Dashboard(BaseModel): class Widget(TimeAuditModel): id = models.UUIDField( - default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True + default=uuid.uuid4, + unique=True, + editable=False, + db_index=True, + primary_key=True, ) key = models.CharField(max_length=255) filters = models.JSONField(default=dict) diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index f3331c874..6a00dc690 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -1,5 +1,4 @@ # Python imports -import uuid # Django imports from django.db import models diff --git a/apiserver/plane/db/models/integration/slack.py b/apiserver/plane/db/models/integration/slack.py index 72df4dfd7..1f07179b7 100644 --- a/apiserver/plane/db/models/integration/slack.py +++ b/apiserver/plane/db/models/integration/slack.py @@ -1,5 +1,4 @@ # Python imports -import uuid # Django imports from django.db import models diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index d5ed4247a..5bd0b3397 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -320,7 +320,7 @@ class IssueAssignee(ProjectBaseModel): class IssueLink(ProjectBaseModel): title = models.CharField(max_length=255, null=True, blank=True) - url = models.URLField() + url = models.TextField() issue = models.ForeignKey( "db.Issue", on_delete=models.CASCADE, related_name="issue_link" ) diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py index b42ae54a9..9138ece9f 100644 --- a/apiserver/plane/db/models/notification.py +++ b/apiserver/plane/db/models/notification.py @@ -5,6 +5,7 @@ from django.conf import settings # Module imports from . import BaseModel + class Notification(BaseModel): workspace = models.ForeignKey( "db.Workspace", related_name="notifications", on_delete=models.CASCADE @@ -105,10 +106,19 @@ class UserNotificationPreference(BaseModel): """Return the user""" return f"<{self.user}>" + class EmailNotificationLog(BaseModel): # receiver - receiver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="email_notifications") - triggered_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="triggered_emails") + receiver = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="email_notifications", + ) + triggered_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="triggered_emails", + ) # entity - can be issues, pages, etc. entity_identifier = models.UUIDField(null=True) entity_name = models.CharField(max_length=255) diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index b93174724..bb4885d14 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -107,6 +107,7 @@ class Project(BaseModel): close_in = models.IntegerField( default=0, validators=[MinValueValidator(0), MaxValueValidator(12)] ) + logo_props = models.JSONField(default=dict) default_state = models.ForeignKey( "db.State", on_delete=models.SET_NULL, diff --git a/apiserver/plane/db/models/social_connection.py b/apiserver/plane/db/models/social_connection.py index 938a73a62..73028e419 100644 --- a/apiserver/plane/db/models/social_connection.py +++ b/apiserver/plane/db/models/social_connection.py @@ -10,7 +10,7 @@ from . import BaseModel class SocialLoginConnection(BaseModel): medium = models.CharField( max_length=20, - choices=(("Google", "google"), ("Github", "github")), + choices=(("Google", "google"), ("Github", "github"), ("Jira", "jira")), default=None, ) last_login_at = models.DateTimeField(default=timezone.now, null=True) diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 0377ccb8b..c9a8b4cb6 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -138,13 +138,13 @@ class User(AbstractBaseUser, PermissionsMixin): super(User, self).save(*args, **kwargs) - @receiver(post_save, sender=User) def create_user_notification(sender, instance, created, **kwargs): # create preferences if created and not instance.is_bot: # Module imports from plane.db.models import UserNotificationPreference + UserNotificationPreference.objects.create( user=instance, property_change=False, diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 112c68bc8..627904a16 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -1,17 +1,11 @@ # Python imports -import json -import os -import requests import uuid -import random -import string # Django imports from django.utils import timezone from django.contrib.auth.hashers import make_password from django.core.validators import validate_email from django.core.exceptions import ValidationError -from django.conf import settings # Third party imports from rest_framework import status @@ -30,9 +24,9 @@ from plane.license.api.serializers import ( from plane.license.api.permissions import ( InstanceAdminPermission, ) -from plane.db.models import User, WorkspaceMember, ProjectMember +from plane.db.models import User from plane.license.utils.encryption import encrypt_data - +from plane.utils.cache import cache_response, invalidate_cache class InstanceEndpoint(BaseAPIView): def get_permissions(self): @@ -44,6 +38,7 @@ class InstanceEndpoint(BaseAPIView): AllowAny(), ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): instance = Instance.objects.first() # get the instance @@ -58,6 +53,7 @@ class InstanceEndpoint(BaseAPIView): data["is_activated"] = True return Response(data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/instances/", user=False) def patch(self, request): # Get the instance instance = Instance.objects.first() @@ -75,6 +71,7 @@ class InstanceAdminEndpoint(BaseAPIView): InstanceAdminPermission, ] + @invalidate_cache(path="/api/instances/", user=False) # Create an instance admin def post(self, request): email = request.data.get("email", False) @@ -104,6 +101,7 @@ class InstanceAdminEndpoint(BaseAPIView): serializer = InstanceAdminSerializer(instance_admin) return Response(serializer.data, status=status.HTTP_201_CREATED) + @cache_response(60 * 60 * 2) def get(self, request): instance = Instance.objects.first() if instance is None: @@ -115,11 +113,10 @@ class InstanceAdminEndpoint(BaseAPIView): serializer = InstanceAdminSerializer(instance_admins, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/instances/", user=False) def delete(self, request, pk): instance = Instance.objects.first() - instance_admin = InstanceAdmin.objects.filter( - instance=instance, pk=pk - ).delete() + InstanceAdmin.objects.filter(instance=instance, pk=pk).delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -128,6 +125,7 @@ class InstanceConfigurationEndpoint(BaseAPIView): InstanceAdminPermission, ] + @cache_response(60 * 60 * 2, user=False) def get(self, request): instance_configurations = InstanceConfiguration.objects.all() serializer = InstanceConfigurationSerializer( @@ -135,6 +133,8 @@ class InstanceConfigurationEndpoint(BaseAPIView): ) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_cache(path="/api/configs/", user=False) + @invalidate_cache(path="/api/mobile-configs/", user=False) def patch(self, request): configurations = InstanceConfiguration.objects.filter( key__in=request.data.keys() @@ -170,6 +170,7 @@ class InstanceAdminSignInEndpoint(BaseAPIView): AllowAny, ] + @invalidate_cache(path="/api/instances/", user=False) def post(self, request): # Check instance first instance = Instance.objects.first() @@ -201,7 +202,7 @@ class InstanceAdminSignInEndpoint(BaseAPIView): email = email.strip().lower() try: validate_email(email) - except ValidationError as e: + except ValidationError: return Response( {"error": "Please provide a valid email address."}, status=status.HTTP_400_BAD_REQUEST, @@ -260,6 +261,7 @@ class SignUpScreenVisitedEndpoint(BaseAPIView): AllowAny, ] + @invalidate_cache(path="/api/instances/", user=False) def post(self, request): instance = Instance.objects.first() if instance is None: diff --git a/apiserver/plane/license/management/commands/configure_instance.py b/apiserver/plane/license/management/commands/configure_instance.py index f81d98cba..9365f07c5 100644 --- a/apiserver/plane/license/management/commands/configure_instance.py +++ b/apiserver/plane/license/management/commands/configure_instance.py @@ -3,7 +3,6 @@ import os # Django imports from django.core.management.base import BaseCommand -from django.conf import settings # Module imports from plane.license.models import InstanceConfiguration diff --git a/apiserver/plane/license/management/commands/register_instance.py b/apiserver/plane/license/management/commands/register_instance.py index 889cd46dc..32a37879f 100644 --- a/apiserver/plane/license/management/commands/register_instance.py +++ b/apiserver/plane/license/management/commands/register_instance.py @@ -1,6 +1,5 @@ # Python imports import json -import requests import secrets # Django imports @@ -56,9 +55,9 @@ class Command(BaseCommand): user_count=payload.get("user_count", 0), ) - self.stdout.write(self.style.SUCCESS(f"Instance registered")) + self.stdout.write(self.style.SUCCESS("Instance registered")) else: self.stdout.write( - self.style.SUCCESS(f"Instance already registered") + self.style.SUCCESS("Instance already registered") ) return diff --git a/apiserver/plane/middleware/api_log_middleware.py b/apiserver/plane/middleware/api_log_middleware.py index a49d43b55..96c62c2fd 100644 --- a/apiserver/plane/middleware/api_log_middleware.py +++ b/apiserver/plane/middleware/api_log_middleware.py @@ -1,4 +1,4 @@ -from plane.db.models import APIToken, APIActivityLog +from plane.db.models import APIActivityLog class APITokenLogMiddleware: @@ -39,6 +39,5 @@ class APITokenLogMiddleware: except Exception as e: print(e) # If the token does not exist, you can decide whether to log this as an invalid attempt - pass return None diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 8f27d4234..a09a55ccf 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -1,4 +1,7 @@ """Development settings""" + +import os + from .common import * # noqa DEBUG = True @@ -14,7 +17,11 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" CACHES = { "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": REDIS_URL, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, } } diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index 90eb04dd5..5a9c3413d 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -1,4 +1,6 @@ """Production settings""" + +import os from .common import * # noqa # SECURITY WARNING: don't run with debug turned on in production! diff --git a/apiserver/plane/settings/redis.py b/apiserver/plane/settings/redis.py index 5b09a1277..628a3d8e6 100644 --- a/apiserver/plane/settings/redis.py +++ b/apiserver/plane/settings/redis.py @@ -1,4 +1,3 @@ -import os import redis from django.conf import settings from urllib.parse import urlparse diff --git a/apiserver/plane/settings/test.py b/apiserver/plane/settings/test.py index 1e2a55144..84153d37a 100644 --- a/apiserver/plane/settings/test.py +++ b/apiserver/plane/settings/test.py @@ -1,4 +1,5 @@ """Test Settings""" + from .common import * # noqa DEBUG = True diff --git a/apiserver/plane/space/views/base.py b/apiserver/plane/space/views/base.py index b75f3dd18..54dac080c 100644 --- a/apiserver/plane/space/views/base.py +++ b/apiserver/plane/space/views/base.py @@ -10,7 +10,6 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError # Third part imports from rest_framework import status -from rest_framework import status from rest_framework.viewsets import ModelViewSet from rest_framework.response import Response from rest_framework.exceptions import APIException @@ -85,11 +84,8 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): ) if isinstance(e, ObjectDoesNotExist): - model_name = str(exc).split(" matching query does not exist.")[ - 0 - ] return Response( - {"error": f"The required object does not exist."}, + {"error": "The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) @@ -179,7 +175,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): if isinstance(e, ObjectDoesNotExist): return Response( - {"error": f"The required object does not exist."}, + {"error": "The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) diff --git a/apiserver/plane/space/views/inbox.py b/apiserver/plane/space/views/inbox.py index 2bf8f8303..9f681c160 100644 --- a/apiserver/plane/space/views/inbox.py +++ b/apiserver/plane/space/views/inbox.py @@ -134,7 +134,7 @@ class InboxIssuePublicViewSet(BaseViewSet): ) # Check for valid priority - if not request.data.get("issue", {}).get("priority", "none") in [ + if request.data.get("issue", {}).get("priority", "none") not in [ "low", "medium", "high", diff --git a/apiserver/plane/space/views/issue.py b/apiserver/plane/space/views/issue.py index d58efff8e..e5cb36eec 100644 --- a/apiserver/plane/space/views/issue.py +++ b/apiserver/plane/space/views/issue.py @@ -9,7 +9,6 @@ from django.db.models import ( Func, F, Q, - Count, Case, Value, CharField, @@ -518,9 +517,13 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): ] def get(self, request, slug, project_id): - project_deploy_board = ProjectDeployBoard.objects.get( + if not ProjectDeployBoard.objects.filter( workspace__slug=slug, project_id=project_id - ) + ).exists(): + return Response( + {"error": "Project is not published"}, + status=status.HTTP_404_NOT_FOUND, + ) filters = issue_filters(request.query_params, "GET") diff --git a/apiserver/plane/space/views/project.py b/apiserver/plane/space/views/project.py index 8cd3f55c5..10a3c3879 100644 --- a/apiserver/plane/space/views/project.py +++ b/apiserver/plane/space/views/project.py @@ -12,7 +12,6 @@ from rest_framework.permissions import AllowAny # Module imports from .base import BaseAPIView from plane.app.serializers import ProjectDeployBoardSerializer -from plane.app.permissions import ProjectMemberPermission from plane.db.models import ( Project, ProjectDeployBoard, diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index 669f3ea73..3b042ea1f 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -7,6 +7,7 @@ from django.views.generic import TemplateView from django.conf import settings +handler404 = "plane.app.views.error_404.custom_404_view" urlpatterns = [ path("", TemplateView.as_view(template_name="index.html")), diff --git a/apiserver/plane/utils/cache.py b/apiserver/plane/utils/cache.py new file mode 100644 index 000000000..dba89c4a6 --- /dev/null +++ b/apiserver/plane/utils/cache.py @@ -0,0 +1,84 @@ +from django.core.cache import cache +# from django.utils.encoding import force_bytes +# import hashlib +from functools import wraps +from rest_framework.response import Response + + +def generate_cache_key(custom_path, auth_header=None): + """Generate a cache key with the given params""" + if auth_header: + key_data = f"{custom_path}:{auth_header}" + else: + key_data = custom_path + return key_data + + +def cache_response(timeout=60 * 60, path=None, user=True): + """decorator to create cache per user""" + + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Function to generate cache key + auth_header = ( + None if request.user.is_anonymous else str(request.user.id) if user else None + ) + custom_path = path if path is not None else request.get_full_path() + key = generate_cache_key(custom_path, auth_header) + cached_result = cache.get(key) + if cached_result is not None: + print("Cache Hit") + return Response( + cached_result["data"], status=cached_result["status"] + ) + + print("Cache Miss") + response = view_func(instance, request, *args, **kwargs) + + if response.status_code == 200: + cache.set( + key, + {"data": response.data, "status": response.status_code}, + timeout, + ) + + return response + + return _wrapped_view + + return decorator + + +def invalidate_cache(path=None, url_params=False, user=True): + """invalidate cache per user""" + + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Invalidate cache before executing the view function + if url_params: + path_with_values = path + for key, value in kwargs.items(): + path_with_values = path_with_values.replace( + f":{key}", str(value) + ) + + custom_path = path_with_values + else: + custom_path = ( + path if path is not None else request.get_full_path() + ) + + auth_header = ( + None if request.user.is_anonymous else str(request.user.id) if user else None + ) + key = generate_cache_key(custom_path, auth_header) + cache.delete(key) + print("Invalidating cache") + # Execute the view function + return view_func(instance, request, *args, **kwargs) + + return _wrapped_view + + return decorator diff --git a/apiserver/plane/utils/importers/__init__.py b/apiserver/plane/utils/importers/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apiserver/plane/utils/importers/jira.py b/apiserver/plane/utils/importers/jira.py deleted file mode 100644 index 6f3a7c217..000000000 --- a/apiserver/plane/utils/importers/jira.py +++ /dev/null @@ -1,117 +0,0 @@ -import requests -import re -from requests.auth import HTTPBasicAuth -from sentry_sdk import capture_exception -from urllib.parse import urlparse, urljoin - - -def is_allowed_hostname(hostname): - allowed_domains = [ - "atl-paas.net", - "atlassian.com", - "atlassian.net", - "jira.com", - ] - parsed_uri = urlparse(f"https://{hostname}") - domain = parsed_uri.netloc.split(":")[0] # Ensures no port is included - base_domain = ".".join(domain.split(".")[-2:]) - return base_domain in allowed_domains - - -def is_valid_project_key(project_key): - if project_key: - project_key = project_key.strip().upper() - # Adjust the regular expression as needed based on your specific requirements. - if len(project_key) > 30: - return False - # Check the validity of the key as well - pattern = re.compile(r"^[A-Z0-9]{1,10}$") - return pattern.match(project_key) is not None - else: - False - - -def generate_valid_project_key(project_key): - return project_key.strip().upper() - - -def generate_url(hostname, path): - if not is_allowed_hostname(hostname): - raise ValueError("Invalid or unauthorized hostname") - return urljoin(f"https://{hostname}", path) - - -def jira_project_issue_summary(email, api_token, project_key, hostname): - try: - if not is_allowed_hostname(hostname): - return {"error": "Invalid or unauthorized hostname"} - - if not is_valid_project_key(project_key): - return {"error": "Invalid project key"} - - auth = HTTPBasicAuth(email, api_token) - headers = {"Accept": "application/json"} - - # make the project key upper case - project_key = generate_valid_project_key(project_key) - - # issues - issue_url = generate_url( - hostname, - f"/rest/api/3/search?jql=project={project_key} AND issuetype!=Epic", - ) - issue_response = requests.request( - "GET", issue_url, headers=headers, auth=auth - ).json()["total"] - - # modules - module_url = generate_url( - hostname, - f"/rest/api/3/search?jql=project={project_key} AND issuetype=Epic", - ) - module_response = requests.request( - "GET", module_url, headers=headers, auth=auth - ).json()["total"] - - # status - status_url = generate_url( - hostname, f"/rest/api/3/project/${project_key}/statuses" - ) - status_response = requests.request( - "GET", status_url, headers=headers, auth=auth - ).json() - - # labels - labels_url = generate_url( - hostname, f"/rest/api/3/label/?jql=project={project_key}" - ) - labels_response = requests.request( - "GET", labels_url, headers=headers, auth=auth - ).json()["total"] - - # users - users_url = generate_url( - hostname, f"/rest/api/3/users/search?jql=project={project_key}" - ) - users_response = requests.request( - "GET", users_url, headers=headers, auth=auth - ).json() - - return { - "issues": issue_response, - "modules": module_response, - "labels": labels_response, - "states": len(status_response), - "users": ( - [ - user - for user in users_response - if user.get("accountType") == "atlassian" - ] - ), - } - except Exception as e: - capture_exception(e) - return { - "error": "Something went wrong could not fetch information from jira" - } diff --git a/apiserver/plane/utils/integrations/__init__.py b/apiserver/plane/utils/integrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apiserver/plane/utils/integrations/github.py b/apiserver/plane/utils/integrations/github.py deleted file mode 100644 index 5a7ce2aa2..000000000 --- a/apiserver/plane/utils/integrations/github.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -import jwt -import requests -from urllib.parse import urlparse, parse_qs -from datetime import datetime, timedelta -from cryptography.hazmat.primitives.serialization import load_pem_private_key -from cryptography.hazmat.backends import default_backend -from django.conf import settings - - -def get_jwt_token(): - app_id = os.environ.get("GITHUB_APP_ID", "") - secret = bytes( - os.environ.get("GITHUB_APP_PRIVATE_KEY", ""), encoding="utf8" - ) - current_timestamp = int(datetime.now().timestamp()) - due_date = datetime.now() + timedelta(minutes=10) - expiry = int(due_date.timestamp()) - payload = { - "iss": app_id, - "sub": app_id, - "exp": expiry, - "iat": current_timestamp, - "aud": "https://github.com/login/oauth/access_token", - } - - priv_rsakey = load_pem_private_key(secret, None, default_backend()) - token = jwt.encode(payload, priv_rsakey, algorithm="RS256") - return token - - -def get_github_metadata(installation_id): - token = get_jwt_token() - - url = f"https://api.github.com/app/installations/{installation_id}" - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - } - response = requests.get(url, headers=headers).json() - return response - - -def get_github_repos(access_tokens_url, repositories_url): - token = get_jwt_token() - - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - } - - oauth_response = requests.post( - access_tokens_url, - headers=headers, - ).json() - - oauth_token = oauth_response.get("token", "") - headers = { - "Authorization": "Bearer " + str(oauth_token), - "Accept": "application/vnd.github+json", - } - response = requests.get( - repositories_url, - headers=headers, - ).json() - return response - - -def delete_github_installation(installation_id): - token = get_jwt_token() - - url = f"https://api.github.com/app/installations/{installation_id}" - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - } - response = requests.delete(url, headers=headers) - return response - - -def get_github_repo_details(access_tokens_url, owner, repo): - token = get_jwt_token() - - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - } - - oauth_response = requests.post( - access_tokens_url, - headers=headers, - ).json() - - oauth_token = oauth_response.get("token") - headers = { - "Authorization": "Bearer " + oauth_token, - "Accept": "application/vnd.github+json", - } - open_issues = requests.get( - f"https://api.github.com/repos/{owner}/{repo}", - headers=headers, - ).json()["open_issues_count"] - - total_labels = 0 - - labels_response = requests.get( - f"https://api.github.com/repos/{owner}/{repo}/labels?per_page=100&page=1", - headers=headers, - ) - - # Check if there are more pages - if len(labels_response.links.keys()): - # get the query parameter of last - last_url = labels_response.links.get("last").get("url") - parsed_url = urlparse(last_url) - last_page_value = parse_qs(parsed_url.query)["page"][0] - total_labels = total_labels + 100 * (int(last_page_value) - 1) - - # Get labels in last page - last_page_labels = requests.get(last_url, headers=headers).json() - total_labels = total_labels + len(last_page_labels) - else: - total_labels = len(labels_response.json()) - - # Currently only supporting upto 100 collaborators - # TODO: Update this function to fetch all collaborators - collaborators = requests.get( - f"https://api.github.com/repos/{owner}/{repo}/collaborators?per_page=100&page=1", - headers=headers, - ).json() - - return open_issues, total_labels, collaborators - - -def get_release_notes(): - token = settings.GITHUB_ACCESS_TOKEN - - if token: - headers = { - "Authorization": "Bearer " + str(token), - "Accept": "application/vnd.github.v3+json", - } - else: - headers = { - "Accept": "application/vnd.github.v3+json", - } - url = "https://api.github.com/repos/makeplane/plane/releases?per_page=5&page=1" - response = requests.get(url, headers=headers) - - if response.status_code != 200: - return {"error": "Unable to render information from Github Repository"} - - return response.json() diff --git a/apiserver/plane/utils/integrations/slack.py b/apiserver/plane/utils/integrations/slack.py deleted file mode 100644 index 0cc5b93b2..000000000 --- a/apiserver/plane/utils/integrations/slack.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import requests - - -def slack_oauth(code): - SLACK_OAUTH_URL = os.environ.get("SLACK_OAUTH_URL", False) - SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID", False) - SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET", False) - - # Oauth Slack - if SLACK_OAUTH_URL and SLACK_CLIENT_ID and SLACK_CLIENT_SECRET: - response = requests.get( - SLACK_OAUTH_URL, - params={ - "code": code, - "client_id": SLACK_CLIENT_ID, - "client_secret": SLACK_CLIENT_SECRET, - }, - ) - return response.json() - return {} diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 87284ff24..2c4cbd471 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -463,7 +463,7 @@ def filter_start_target_date_issues(params, filter, method): filter["target_date__isnull"] = False filter["start_date__isnull"] = False return filter - + def issue_filters(query_params, method): filter = {} diff --git a/apiserver/plane/utils/issue_search.py b/apiserver/plane/utils/issue_search.py index 3b6dea332..74d1e8019 100644 --- a/apiserver/plane/utils/issue_search.py +++ b/apiserver/plane/utils/issue_search.py @@ -5,7 +5,6 @@ import re from django.db.models import Q # Module imports -from plane.db.models import Issue def search_issues(query, queryset): diff --git a/apiserver/plane/utils/paginator.py b/apiserver/plane/utils/paginator.py index edb7212bf..db8e1bd94 100644 --- a/apiserver/plane/utils/paginator.py +++ b/apiserver/plane/utils/paginator.py @@ -400,7 +400,7 @@ class BasePaginator: cursor_result = paginator.get_result( limit=per_page, cursor=input_cursor ) - except BadPaginationError as e: + except BadPaginationError: raise ParseError(detail="Error in parsing") if on_results: diff --git a/apiserver/plane/web/views.py b/apiserver/plane/web/views.py index 91ea44a21..60f00ef0e 100644 --- a/apiserver/plane/web/views.py +++ b/apiserver/plane/web/views.py @@ -1,3 +1 @@ -from django.shortcuts import render - # Create your views here. diff --git a/apiserver/pyproject.toml b/apiserver/pyproject.toml index 773d6090e..a6c07b855 100644 --- a/apiserver/pyproject.toml +++ b/apiserver/pyproject.toml @@ -16,3 +16,10 @@ exclude = ''' | venv )/ ''' + +[tool.ruff] +line-length = 79 +exclude = [ + "**/__init__.py", +] + diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index 88ea66c4c..08bc35b28 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -31,11 +31,11 @@ curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-cl ``` NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture + -- - Expect this after a successful install ![Install Output](images/install.png) @@ -50,29 +50,33 @@ Plane App is available via the command `plane-app`. Running the command `plane-a ![Plane Help](images/help.png) -Basic Operations: +Basic Operations: + 1. Start Server using `plane-app start` 1. Stop Server using `plane-app stop` 1. Restart Server using `plane-app restart` Advanced Operations: + 1. Configure Plane using `plane-app --configure`. This will give you options to modify - - NGINX Port (default 80) - - Domain Name (default is the local server public IP address) - - File Upload Size (default 5MB) - - External Postgres DB Url (optional - default empty) - - External Redis URL (optional - default empty) - - AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket) + + - NGINX Port (default 80) + - Domain Name (default is the local server public IP address) + - File Upload Size (default 5MB) + - External Postgres DB Url (optional - default empty) + - External Redis URL (optional - default empty) + - AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket) 1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images) -1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility. +1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility. -1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio. +1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio. -1. Plane App can be reinstalled using `plane-app --install`. +1. Plane App can be reinstalled using `plane-app --install`. + +Application Data is stored in the mentioned folders: -Application Data is stored in the mentioned folders: 1. DB Data: /opt/plane/data/postgres 1. Redis Data: /opt/plane/data/redis -1. Minio Data: /opt/plane/data/minio \ No newline at end of file +1. Minio Data: /opt/plane/data/minio diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index fcb6b57bb..571fb8588 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -53,14 +53,13 @@ "react-moveable": "^0.54.2", "tailwind-merge": "^1.14.0", "tippy.js": "^6.3.7", - "tiptap-markdown": "^0.8.2" + "tiptap-markdown": "^0.8.9" }, "devDependencies": { "@types/node": "18.15.3", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", - "eslint": "^7.32.0", - "eslint-config-next": "13.2.4", + "eslint-config-custom": "*", "postcss": "^8.4.29", "tailwind-config-custom": "*", "tsconfig": "*", diff --git a/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts b/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts new file mode 100644 index 000000000..062acafcb --- /dev/null +++ b/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts @@ -0,0 +1,17 @@ +import { Selection } from "@tiptap/pm/state"; +import { Editor } from "@tiptap/react"; +import { MutableRefObject } from "react"; + +export const insertContentAtSavedSelection = ( + editorRef: MutableRefObject, + content: string, + savedSelection: Selection +) => { + if (editorRef.current && savedSelection) { + editorRef.current + .chain() + .focus() + .insertContentAt(savedSelection?.anchor, content) + .run(); + } +}; diff --git a/packages/editor/core/src/hooks/use-editor.tsx b/packages/editor/core/src/hooks/use-editor.tsx index c2923c1e9..7e6aa5912 100644 --- a/packages/editor/core/src/hooks/use-editor.tsx +++ b/packages/editor/core/src/hooks/use-editor.tsx @@ -1,5 +1,5 @@ import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; -import { useImperativeHandle, useRef, MutableRefObject } from "react"; +import { useImperativeHandle, useRef, MutableRefObject, useState } from "react"; import { CoreEditorProps } from "src/ui/props"; import { CoreEditorExtensions } from "src/ui/extensions"; import { EditorProps } from "@tiptap/pm/view"; @@ -8,6 +8,8 @@ import { DeleteImage } from "src/types/delete-image"; import { IMentionSuggestion } from "src/types/mention-suggestion"; import { RestoreImage } from "src/types/restore-image"; import { UploadImage } from "src/types/upload-image"; +import { Selection } from "@tiptap/pm/state"; +import { insertContentAtSavedSelection } from "src/helpers/insert-content-at-cursor-position"; interface CustomEditorProps { uploadFile: UploadImage; @@ -70,8 +72,10 @@ export const useEditor = ({ onCreate: async ({ editor }) => { onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); }, + onTransaction: async ({ editor }) => { + setSavedSelection(editor.state.selection); + }, onUpdate: async ({ editor }) => { - // for instant feedback loop setIsSubmitting?.("submitting"); setShouldShowAlert?.(true); onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); @@ -83,6 +87,8 @@ export const useEditor = ({ const editorRef: MutableRefObject = useRef(null); editorRef.current = editor; + const [savedSelection, setSavedSelection] = useState(null); + useImperativeHandle(forwardedRef, () => ({ clearEditor: () => { editorRef.current?.commands.clearContent(); @@ -90,6 +96,11 @@ export const useEditor = ({ setEditorValue: (content: string) => { editorRef.current?.commands.setContent(content); }, + setEditorValueAtCursorPosition: (content: string) => { + if (savedSelection) { + insertContentAtSavedSelection(editorRef, content, savedSelection); + } + }, })); if (!editor) { diff --git a/packages/editor/core/src/styles/table.css b/packages/editor/core/src/styles/table.css index ca384d34f..3ba17ee1b 100644 --- a/packages/editor/core/src/styles/table.css +++ b/packages/editor/core/src/styles/table.css @@ -98,7 +98,7 @@ top: 0; bottom: -2px; width: 4px; - z-index: 99; + z-index: 5; background-color: #d9e4ff; pointer-events: none; } @@ -111,7 +111,7 @@ .tableWrapper .tableControls .rowsControl { transition: opacity ease-in 100ms; position: absolute; - z-index: 99; + z-index: 5; display: flex; justify-content: center; align-items: center; @@ -198,7 +198,7 @@ .tableWrapper .tableControls .tableToolbox .toolboxItem:hover, .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem:hover { - background-color: rgba(var(--color-background-100), 0.5); + background-color: rgba(var(--color-background-80), 0.6); } .tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer, diff --git a/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts b/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts index 0854092a9..ec6c540da 100644 --- a/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts +++ b/packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts @@ -15,9 +15,15 @@ export function clickHandler(options: ClickHandlerOptions): Plugin { return false; } - const eventTarget = event.target as HTMLElement; + let a = event.target as HTMLElement; + const els = []; - if (eventTarget.nodeName !== "A") { + while (a.nodeName !== "DIV") { + els.push(a); + a = a.parentNode as HTMLElement; + } + + if (!els.find((value) => value.nodeName === "A")) { return false; } @@ -28,9 +34,7 @@ export function clickHandler(options: ClickHandlerOptions): Plugin { const target = link?.target ?? attrs.target; if (link && href) { - if (view.editable) { - window.open(href, target); - } + window.open(href, target); return true; } diff --git a/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts b/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts index 83e38054c..475bf28d9 100644 --- a/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts +++ b/packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts @@ -33,16 +33,8 @@ export function pasteHandler(options: PasteHandlerOptions): Plugin { return false; } - const html = event.clipboardData?.getData("text/html"); - - const hrefRegex = /href="([^"]*)"/; - - const existingLink = html?.match(hrefRegex); - - const url = existingLink ? existingLink[1] : link.href; - options.editor.commands.setMark(options.type, { - href: url, + href: link.href, }); return true; diff --git a/packages/editor/core/src/ui/extensions/custom-link/index.tsx b/packages/editor/core/src/ui/extensions/custom-link/index.ts similarity index 69% rename from packages/editor/core/src/ui/extensions/custom-link/index.tsx rename to packages/editor/core/src/ui/extensions/custom-link/index.ts index e66d18904..88e7abfe5 100644 --- a/packages/editor/core/src/ui/extensions/custom-link/index.tsx +++ b/packages/editor/core/src/ui/extensions/custom-link/index.ts @@ -1,41 +1,76 @@ -import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core"; +import { Mark, markPasteRule, mergeAttributes, PasteRuleMatch } from "@tiptap/core"; import { Plugin } from "@tiptap/pm/state"; import { find, registerCustomProtocol, reset } from "linkifyjs"; - -import { autolink } from "src/ui/extensions/custom-link/helpers/autolink"; -import { clickHandler } from "src/ui/extensions/custom-link/helpers/clickHandler"; -import { pasteHandler } from "src/ui/extensions/custom-link/helpers/pasteHandler"; +import { autolink } from "./helpers/autolink"; +import { clickHandler } from "./helpers/clickHandler"; +import { pasteHandler } from "./helpers/pasteHandler"; export interface LinkProtocolOptions { scheme: string; optionalSlashes?: boolean; } +export const pasteRegex = + /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi; + export interface LinkOptions { + /** + * If enabled, it adds links as you type. + */ autolink: boolean; - inclusive: boolean; + /** + * An array of custom protocols to be registered with linkifyjs. + */ protocols: Array; + /** + * If enabled, links will be opened on click. + */ openOnClick: boolean; + /** + * If enabled, links will be inclusive i.e. if you move your cursor to the + * link text, and start typing, it'll be a part of the link itself. + */ + inclusive: boolean; + /** + * Adds a link to the current selection if the pasted content only contains an url. + */ linkOnPaste: boolean; + /** + * A list of HTML attributes to be rendered. + */ HTMLAttributes: Record; + /** + * A validation function that modifies link verification for the auto linker. + * @param url - The url to be validated. + * @returns - True if the url is valid, false otherwise. + */ validate?: (url: string) => boolean; } declare module "@tiptap/core" { interface Commands { link: { + /** + * Set a link mark + */ setLink: (attributes: { href: string; target?: string | null; rel?: string | null; class?: string | null; }) => ReturnType; + /** + * Toggle a link mark + */ toggleLink: (attributes: { href: string; target?: string | null; rel?: string | null; class?: string | null; }) => ReturnType; + /** + * Unset a link mark + */ unsetLink: () => ReturnType; }; } @@ -150,37 +185,31 @@ export const CustomLinkExtension = Mark.create({ addPasteRules() { return [ markPasteRule({ - find: (text) => - find(text) - .filter((link) => { - if (this.options.validate) { - return this.options.validate(link.value); - } - return true; - }) - .filter((link) => link.isLink) - .map((link) => ({ - text: link.value, - index: link.start, - data: link, - })), - type: this.type, - getAttributes: (match, pasteEvent) => { - const html = pasteEvent?.clipboardData?.getData("text/html"); - const hrefRegex = /href="([^"]*)"/; + find: (text) => { + const foundLinks: PasteRuleMatch[] = []; - const existingLink = html?.match(hrefRegex); + if (text) { + const links = find(text).filter((item) => item.isLink); - if (existingLink) { - return { - href: existingLink[1], - }; + if (links.length) { + links.forEach((link) => + foundLinks.push({ + text: link.value, + data: { + href: link.href, + }, + index: link.start, + }) + ); + } } - return { - href: match.data?.href, - }; + return foundLinks; }, + type: this.type, + getAttributes: (match) => ({ + href: match.data?.href, + }), }), ]; }, diff --git a/packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts b/packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts new file mode 100644 index 000000000..2af845b7a --- /dev/null +++ b/packages/editor/core/src/ui/extensions/horizontal-rule/horizontal-rule.ts @@ -0,0 +1,111 @@ +import { isNodeSelection, mergeAttributes, Node, nodeInputRule } from "@tiptap/core"; +import { NodeSelection, TextSelection } from "@tiptap/pm/state"; + +export interface HorizontalRuleOptions { + HTMLAttributes: Record; +} + +declare module "@tiptap/core" { + interface Commands { + horizontalRule: { + /** + * Add a horizontal rule + */ + setHorizontalRule: () => ReturnType; + }; + } +} + +export const CustomHorizontalRule = Node.create({ + name: "horizontalRule", + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + group: "block", + + parseHTML() { + return [{ tag: "hr" }]; + }, + + renderHTML({ HTMLAttributes }) { + return ["hr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; + }, + + addCommands() { + return { + setHorizontalRule: + () => + ({ chain, state }) => { + const { selection } = state; + const { $from: $originFrom, $to: $originTo } = selection; + + const currentChain = chain(); + + if ($originFrom.parentOffset === 0) { + currentChain.insertContentAt( + { + from: Math.max($originFrom.pos - 1, 0), + to: $originTo.pos, + }, + { + type: this.name, + } + ); + } else if (isNodeSelection(selection)) { + currentChain.insertContentAt($originTo.pos, { + type: this.name, + }); + } else { + currentChain.insertContent({ type: this.name }); + } + + return ( + currentChain + // set cursor after horizontal rule + .command(({ tr, dispatch }) => { + if (dispatch) { + const { $to } = tr.selection; + const posAfter = $to.end(); + + if ($to.nodeAfter) { + if ($to.nodeAfter.isTextblock) { + tr.setSelection(TextSelection.create(tr.doc, $to.pos + 1)); + } else if ($to.nodeAfter.isBlock) { + tr.setSelection(NodeSelection.create(tr.doc, $to.pos)); + } else { + tr.setSelection(TextSelection.create(tr.doc, $to.pos)); + } + } else { + // add node after horizontal rule if it’s the end of the document + const node = $to.parent.type.contentMatch.defaultType?.create(); + + if (node) { + tr.insert(posAfter, node); + tr.setSelection(TextSelection.create(tr.doc, posAfter + 1)); + } + } + + tr.scrollIntoView(); + } + + return true; + }) + .run() + ); + }, + }; + }, + + addInputRules() { + return [ + nodeInputRule({ + find: /^(?:---|—-|___\s|\*\*\*\s)$/, + type: this.type, + }), + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 190731fe0..7da381e98 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -27,6 +27,7 @@ import { RestoreImage } from "src/types/restore-image"; import { CustomLinkExtension } from "src/ui/extensions/custom-link"; import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline"; import { CustomTypographyExtension } from "src/ui/extensions/typography"; +import { CustomHorizontalRule } from "./horizontal-rule/horizontal-rule"; export const CoreEditorExtensions = ( mentionConfig: { @@ -55,9 +56,7 @@ export const CoreEditorExtensions = ( }, code: false, codeBlock: false, - horizontalRule: { - HTMLAttributes: { class: "mt-4 mb-4" }, - }, + horizontalRule: false, blockquote: false, dropcursor: { color: "rgba(var(--color-text-100))", @@ -67,6 +66,10 @@ export const CoreEditorExtensions = ( CustomQuoteExtension.configure({ HTMLAttributes: { className: "border-l-4 border-custom-border-300" }, }), + + CustomHorizontalRule.configure({ + HTMLAttributes: { class: "mt-4 mb-4" }, + }), CustomKeymap, ListKeymap, CustomLinkExtension.configure({ diff --git a/packages/editor/core/src/ui/extensions/table/table/table-view.tsx b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx index 674a8e115..2941179c7 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table-view.tsx +++ b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx @@ -213,10 +213,11 @@ function createToolbox({ { className: "colorPicker grid" }, Object.entries(colors).map(([colorName, colorValue]) => h("div", { - className: "colorPickerItem", + className: "colorPickerItem flex items-center justify-center", style: `background-color: ${colorValue.backgroundColor}; - color: ${colorValue.textColor || "inherit"};`, - innerHTML: colorValue?.icon || "", + color: ${colorValue.textColor || "inherit"};`, + innerHTML: + colorValue.icon ?? `A`, onClick: () => onSelectColor(colorValue), }) ) diff --git a/packages/editor/document-editor/package.json b/packages/editor/document-editor/package.json index bd1f2d90f..870d5edd9 100644 --- a/packages/editor/document-editor/package.json +++ b/packages/editor/document-editor/package.json @@ -37,7 +37,6 @@ "@tiptap/extension-placeholder": "^2.1.13", "@tiptap/pm": "^2.1.13", "@tiptap/suggestion": "^2.1.13", - "eslint-config-next": "13.2.4", "lucide-react": "^0.309.0", "react-popper": "^2.3.0", "tippy.js": "^6.3.7", @@ -47,7 +46,7 @@ "@types/node": "18.15.3", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", - "eslint": "8.36.0", + "eslint-config-custom": "*", "postcss": "^8.4.29", "tailwind-config-custom": "*", "tsconfig": "*", diff --git a/packages/editor/document-editor/src/ui/components/content-browser.tsx b/packages/editor/document-editor/src/ui/components/content-browser.tsx index 97231ea96..be70067a2 100644 --- a/packages/editor/document-editor/src/ui/components/content-browser.tsx +++ b/packages/editor/document-editor/src/ui/components/content-browser.tsx @@ -15,7 +15,7 @@ export const ContentBrowser = (props: ContentBrowserProps) => { const handleOnClick = (marking: IMarking) => { scrollSummary(editor, marking); if (setSidePeekVisible) setSidePeekVisible(false); - } + }; return (
diff --git a/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx b/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx index 136d04e01..971915439 100644 --- a/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx +++ b/packages/editor/document-editor/src/ui/components/links/link-edit-view.tsx @@ -40,9 +40,11 @@ export const LinkEditView = ({ const [positionRef, setPositionRef] = useState({ from: from, to: to }); const [localUrl, setLocalUrl] = useState(viewProps.url); - const linkRemoved = useRef(); + const linkRemoved = useRef(); const getText = (from: number, to: number) => { + if (to >= editor.state.doc.content.size) return ""; + const text = editor.state.doc.textBetween(from, to, "\n"); return text; }; @@ -72,10 +74,12 @@ export const LinkEditView = ({ const url = isValidUrl(localUrl) ? localUrl : viewProps.url; + if (to >= editor.state.doc.content.size) return; + editor.view.dispatch(editor.state.tr.removeMark(from, to, editor.schema.marks.link)); editor.view.dispatch(editor.state.tr.addMark(from, to, editor.schema.marks.link.create({ href: url }))); }, - [localUrl] + [localUrl, editor, from, to, viewProps.url] ); const handleUpdateText = (text: string) => { diff --git a/packages/editor/document-editor/src/ui/components/summary-popover.tsx b/packages/editor/document-editor/src/ui/components/summary-popover.tsx index 6ad7cad83..41056c6ad 100644 --- a/packages/editor/document-editor/src/ui/components/summary-popover.tsx +++ b/packages/editor/document-editor/src/ui/components/summary-popover.tsx @@ -33,8 +33,9 @@ export const SummaryPopover: React.FC = (props) => { + + +
+ tab.key === defaultOpen)} + > + + {TABS_LIST.map((tab) => ( + + cn("py-1 text-sm rounded border border-custom-border-200", { + "bg-custom-background-80": selected, + "hover:bg-custom-background-90 focus:bg-custom-background-90": !selected, + }) + } + > + {tab.title} + + ))} + + + + { + onChange({ + type: EmojiIconPickerTypes.EMOJI, + value: val, + }); + if (closeOnSelect) close(); + }} + height="20rem" + width="100%" + theme={theme} + searchPlaceholder={searchPlaceholder} + previewConfig={{ + showPreview: false, + }} + /> + + + { + onChange({ + type: EmojiIconPickerTypes.ICON, + value: val, + }); + if (closeOnSelect) close(); + }} + /> + + + +
+
+ + )} + + ); +}; diff --git a/packages/ui/src/emoji/icons-list.tsx b/packages/ui/src/emoji/icons-list.tsx new file mode 100644 index 000000000..f55da881b --- /dev/null +++ b/packages/ui/src/emoji/icons-list.tsx @@ -0,0 +1,110 @@ +import React, { useEffect, useState } from "react"; +// components +import { Input } from "../form-fields"; +// helpers +import { cn } from "../../helpers"; +// constants +import { MATERIAL_ICONS_LIST } from "./icons"; + +type TIconsListProps = { + defaultColor: string; + onChange: (val: { name: string; color: string }) => void; +}; + +const DEFAULT_COLORS = ["#ff6b00", "#8cc1ff", "#fcbe1d", "#18904f", "#adf672", "#05c3ff", "#5f5f5f"]; + +export const IconsList: React.FC = (props) => { + const { defaultColor, onChange } = props; + // states + const [activeColor, setActiveColor] = useState(defaultColor); + const [showHexInput, setShowHexInput] = useState(false); + const [hexValue, setHexValue] = useState(""); + + useEffect(() => { + if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false); + else { + setHexValue(defaultColor.slice(1, 7)); + setShowHexInput(true); + } + }, [defaultColor]); + + return ( + <> +
+ {showHexInput ? ( +
+ + HEX + # + { + const value = e.target.value; + setHexValue(value); + if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(`#${value}`); + }} + className="flex-grow pl-0 text-xs text-custom-text-200" + mode="true-transparent" + autoFocus + /> +
+ ) : ( + DEFAULT_COLORS.map((curCol) => ( + + )) + )} + +
+
+ {MATERIAL_ICONS_LIST.map((icon) => ( + + ))} +
+ + ); +}; diff --git a/packages/ui/src/emoji/icons.ts b/packages/ui/src/emoji/icons.ts new file mode 100644 index 000000000..72aacf18b --- /dev/null +++ b/packages/ui/src/emoji/icons.ts @@ -0,0 +1,605 @@ +export const MATERIAL_ICONS_LIST = [ + { + name: "search", + }, + { + name: "home", + }, + { + name: "menu", + }, + { + name: "close", + }, + { + name: "settings", + }, + { + name: "done", + }, + { + name: "check_circle", + }, + { + name: "favorite", + }, + { + name: "add", + }, + { + name: "delete", + }, + { + name: "arrow_back", + }, + { + name: "star", + }, + { + name: "logout", + }, + { + name: "add_circle", + }, + { + name: "cancel", + }, + { + name: "arrow_drop_down", + }, + { + name: "more_vert", + }, + { + name: "check", + }, + { + name: "check_box", + }, + { + name: "toggle_on", + }, + { + name: "open_in_new", + }, + { + name: "refresh", + }, + { + name: "login", + }, + { + name: "radio_button_unchecked", + }, + { + name: "more_horiz", + }, + { + name: "apps", + }, + { + name: "radio_button_checked", + }, + { + name: "download", + }, + { + name: "remove", + }, + { + name: "toggle_off", + }, + { + name: "bolt", + }, + { + name: "arrow_upward", + }, + { + name: "filter_list", + }, + { + name: "delete_forever", + }, + { + name: "autorenew", + }, + { + name: "key", + }, + { + name: "sort", + }, + { + name: "sync", + }, + { + name: "add_box", + }, + { + name: "block", + }, + { + name: "restart_alt", + }, + { + name: "menu_open", + }, + { + name: "shopping_cart_checkout", + }, + { + name: "expand_circle_down", + }, + { + name: "backspace", + }, + { + name: "undo", + }, + { + name: "done_all", + }, + { + name: "do_not_disturb_on", + }, + { + name: "open_in_full", + }, + { + name: "double_arrow", + }, + { + name: "sync_alt", + }, + { + name: "zoom_in", + }, + { + name: "done_outline", + }, + { + name: "drag_indicator", + }, + { + name: "fullscreen", + }, + { + name: "star_half", + }, + { + name: "settings_accessibility", + }, + { + name: "reply", + }, + { + name: "exit_to_app", + }, + { + name: "unfold_more", + }, + { + name: "library_add", + }, + { + name: "cached", + }, + { + name: "select_check_box", + }, + { + name: "terminal", + }, + { + name: "change_circle", + }, + { + name: "disabled_by_default", + }, + { + name: "swap_horiz", + }, + { + name: "swap_vert", + }, + { + name: "app_registration", + }, + { + name: "download_for_offline", + }, + { + name: "close_fullscreen", + }, + { + name: "file_open", + }, + { + name: "minimize", + }, + { + name: "open_with", + }, + { + name: "dataset", + }, + { + name: "add_task", + }, + { + name: "start", + }, + { + name: "keyboard_voice", + }, + { + name: "create_new_folder", + }, + { + name: "forward", + }, + { + name: "download", + }, + { + name: "settings_applications", + }, + { + name: "compare_arrows", + }, + { + name: "redo", + }, + { + name: "zoom_out", + }, + { + name: "publish", + }, + { + name: "html", + }, + { + name: "token", + }, + { + name: "switch_access_shortcut", + }, + { + name: "fullscreen_exit", + }, + { + name: "sort_by_alpha", + }, + { + name: "delete_sweep", + }, + { + name: "indeterminate_check_box", + }, + { + name: "view_timeline", + }, + { + name: "settings_backup_restore", + }, + { + name: "arrow_drop_down_circle", + }, + { + name: "assistant_navigation", + }, + { + name: "sync_problem", + }, + { + name: "clear_all", + }, + { + name: "density_medium", + }, + { + name: "heart_plus", + }, + { + name: "filter_alt_off", + }, + { + name: "expand", + }, + { + name: "subdirectory_arrow_right", + }, + { + name: "download_done", + }, + { + name: "arrow_outward", + }, + { + name: "123", + }, + { + name: "swipe_left", + }, + { + name: "auto_mode", + }, + { + name: "saved_search", + }, + { + name: "place_item", + }, + { + name: "system_update_alt", + }, + { + name: "javascript", + }, + { + name: "search_off", + }, + { + name: "output", + }, + { + name: "select_all", + }, + { + name: "fit_screen", + }, + { + name: "swipe_up", + }, + { + name: "dynamic_form", + }, + { + name: "hide_source", + }, + { + name: "swipe_right", + }, + { + name: "switch_access_shortcut_add", + }, + { + name: "browse_gallery", + }, + { + name: "css", + }, + { + name: "density_small", + }, + { + name: "assistant_direction", + }, + { + name: "check_small", + }, + { + name: "youtube_searched_for", + }, + { + name: "move_up", + }, + { + name: "swap_horizontal_circle", + }, + { + name: "data_thresholding", + }, + { + name: "install_mobile", + }, + { + name: "move_down", + }, + { + name: "dataset_linked", + }, + { + name: "keyboard_command_key", + }, + { + name: "view_kanban", + }, + { + name: "swipe_down", + }, + { + name: "key_off", + }, + { + name: "transcribe", + }, + { + name: "send_time_extension", + }, + { + name: "swipe_down_alt", + }, + { + name: "swipe_left_alt", + }, + { + name: "swipe_right_alt", + }, + { + name: "swipe_up_alt", + }, + { + name: "keyboard_option_key", + }, + { + name: "cycle", + }, + { + name: "rebase", + }, + { + name: "rebase_edit", + }, + { + name: "empty_dashboard", + }, + { + name: "magic_exchange", + }, + { + name: "acute", + }, + { + name: "point_scan", + }, + { + name: "step_into", + }, + { + name: "cheer", + }, + { + name: "emoticon", + }, + { + name: "explosion", + }, + { + name: "water_bottle", + }, + { + name: "weather_hail", + }, + { + name: "syringe", + }, + { + name: "pill", + }, + { + name: "genetics", + }, + { + name: "allergy", + }, + { + name: "medical_mask", + }, + { + name: "body_fat", + }, + { + name: "barefoot", + }, + { + name: "infrared", + }, + { + name: "wrist", + }, + { + name: "metabolism", + }, + { + name: "conditions", + }, + { + name: "taunt", + }, + { + name: "altitude", + }, + { + name: "tibia", + }, + { + name: "footprint", + }, + { + name: "eyeglasses", + }, + { + name: "man_3", + }, + { + name: "woman_2", + }, + { + name: "rheumatology", + }, + { + name: "tornado", + }, + { + name: "landslide", + }, + { + name: "foggy", + }, + { + name: "severe_cold", + }, + { + name: "tsunami", + }, + { + name: "vape_free", + }, + { + name: "sign_language", + }, + { + name: "emoji_symbols", + }, + { + name: "clear_night", + }, + { + name: "emoji_food_beverage", + }, + { + name: "hive", + }, + { + name: "thunderstorm", + }, + { + name: "communication", + }, + { + name: "rocket", + }, + { + name: "pets", + }, + { + name: "public", + }, + { + name: "quiz", + }, + { + name: "mood", + }, + { + name: "gavel", + }, + { + name: "eco", + }, + { + name: "diamond", + }, + { + name: "forest", + }, + { + name: "rainy", + }, + { + name: "skull", + }, +]; diff --git a/packages/ui/src/emoji/index.ts b/packages/ui/src/emoji/index.ts new file mode 100644 index 000000000..973454139 --- /dev/null +++ b/packages/ui/src/emoji/index.ts @@ -0,0 +1 @@ +export * from "./emoji-icon-picker"; diff --git a/packages/ui/src/form-fields/input.tsx b/packages/ui/src/form-fields/input.tsx index 6688d6778..f73467621 100644 --- a/packages/ui/src/form-fields/input.tsx +++ b/packages/ui/src/form-fields/input.tsx @@ -1,4 +1,6 @@ import * as React from "react"; +// helpers +import { cn } from "../../helpers"; export interface InputProps extends React.InputHTMLAttributes { mode?: "primary" | "transparent" | "true-transparent"; @@ -16,17 +18,20 @@ const Input = React.forwardRef((props, ref) => { ref={ref} type={type} name={name} - className={`block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${ - mode === "primary" - ? "rounded-md border-[0.5px] border-custom-border-200" - : mode === "transparent" - ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" - : mode === "true-transparent" - ? "rounded border-none bg-transparent ring-0" - : "" - } ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${ - inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : "" - } ${className}`} + className={cn( + `block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${ + mode === "primary" + ? "rounded-md border-[0.5px] border-custom-border-200" + : mode === "transparent" + ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" + : mode === "true-transparent" + ? "rounded border-none bg-transparent ring-0" + : "" + } ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${ + inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : "" + }`, + className + )} {...rest} /> ); diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index b90b6993a..24b76c3e0 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -2,6 +2,7 @@ export * from "./avatar"; export * from "./breadcrumbs"; export * from "./badge"; export * from "./button"; +export * from "./emoji"; export * from "./dropdowns"; export * from "./form-fields"; export * from "./icons"; @@ -10,3 +11,4 @@ export * from "./spinners"; export * from "./tooltip"; export * from "./loader"; export * from "./control-link"; +export * from "./toast"; diff --git a/packages/ui/src/spinners/circular-bar-spinner.tsx b/packages/ui/src/spinners/circular-bar-spinner.tsx new file mode 100644 index 000000000..3be8af43a --- /dev/null +++ b/packages/ui/src/spinners/circular-bar-spinner.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; + +interface ICircularBarSpinner extends React.SVGAttributes { + height?: string; + width?: string; + className?: string | undefined; +} + +export const CircularBarSpinner: React.FC = ({ + height = "16px", + width = "16px", + className = "", +}) => ( +
+ + + + + + + + + + + + +
+); diff --git a/packages/ui/src/spinners/index.ts b/packages/ui/src/spinners/index.ts index 768568172..a871a9b77 100644 --- a/packages/ui/src/spinners/index.ts +++ b/packages/ui/src/spinners/index.ts @@ -1 +1,2 @@ export * from "./circular-spinner"; +export * from "./circular-bar-spinner"; diff --git a/packages/ui/src/toast/index.tsx b/packages/ui/src/toast/index.tsx new file mode 100644 index 000000000..f38050532 --- /dev/null +++ b/packages/ui/src/toast/index.tsx @@ -0,0 +1,210 @@ +import * as React from "react"; +import { Toaster, toast } from "sonner"; +// icons +import { AlertTriangle, CheckCircle2, X, XCircle } from "lucide-react"; +// spinner +import { CircularBarSpinner } from "../spinners"; +// helper +import { cn } from "../../helpers"; + +export enum TOAST_TYPE { + SUCCESS = "success", + ERROR = "error", + INFO = "info", + WARNING = "warning", + LOADING = "loading", +} + +type SetToastProps = + | { + type: TOAST_TYPE.LOADING; + title?: string; + } + | { + id?: string | number; + type: Exclude; + title: string; + message?: string; + }; + +type PromiseToastCallback = (data: ToastData) => string; + +type PromiseToastData = { + title: string; + message?: PromiseToastCallback; +}; + +type PromiseToastOptions = { + loading?: string; + success: PromiseToastData; + error: PromiseToastData; +}; + +type ToastContentProps = { + toastId: string | number; + icon?: React.ReactNode; + textColorClassName: string; + backgroundColorClassName: string; + borderColorClassName: string; +}; + +type ToastProps = { + theme: "light" | "dark" | "system"; +}; + +export const Toast = (props: ToastProps) => { + const { theme } = props; + return ; +}; + +export const setToast = (props: SetToastProps) => { + const renderToastContent = ({ + toastId, + icon, + textColorClassName, + backgroundColorClassName, + borderColorClassName, + }: ToastContentProps) => + props.type === TOAST_TYPE.LOADING ? ( +
{ + e.stopPropagation(); + e.preventDefault(); + }} + className={cn( + "w-[350px] h-[67.3px] rounded-lg border shadow-sm p-2", + backgroundColorClassName, + borderColorClassName + )} + > +
+ {icon &&
{icon}
} +
+
{props.title ?? "Loading..."}
+
+ toast.dismiss(toastId)} + /> +
+
+
+
+ ) : ( +
{ + e.stopPropagation(); + e.preventDefault(); + }} + className={cn( + "relative flex flex-col w-[350px] rounded-lg border shadow-sm p-2", + backgroundColorClassName, + borderColorClassName + )} + > + toast.dismiss(toastId)} + /> +
+ {icon &&
{icon}
} +
+
{props.title}
+ {props.message &&
{props.message}
} +
+
+
+ ); + + switch (props.type) { + case TOAST_TYPE.SUCCESS: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-success", + backgroundColorClassName: "bg-toast-background-success", + borderColorClassName: "border-toast-border-success", + }), + props.id ? { id: props.id } : {} + ); + case TOAST_TYPE.ERROR: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-error", + backgroundColorClassName: "bg-toast-background-error", + borderColorClassName: "border-toast-border-error", + }), + props.id ? { id: props.id } : {} + ); + case TOAST_TYPE.WARNING: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-warning", + backgroundColorClassName: "bg-toast-background-warning", + borderColorClassName: "border-toast-border-warning", + }), + props.id ? { id: props.id } : {} + ); + case TOAST_TYPE.INFO: + return toast.custom( + (toastId) => + renderToastContent({ + toastId, + textColorClassName: "text-toast-text-info", + backgroundColorClassName: "bg-toast-background-info", + borderColorClassName: "border-toast-border-info", + }), + props.id ? { id: props.id } : {} + ); + + case TOAST_TYPE.LOADING: + return toast.custom((toastId) => + renderToastContent({ + toastId, + icon: , + textColorClassName: "text-toast-text-loading", + backgroundColorClassName: "bg-toast-background-loading", + borderColorClassName: "border-toast-border-loading", + }) + ); + } +}; + +export const setPromiseToast = ( + promise: Promise, + options: PromiseToastOptions +): void => { + const tId = setToast({ type: TOAST_TYPE.LOADING, title: options.loading }); + + promise + .then((data: ToastData) => { + setToast({ + type: TOAST_TYPE.SUCCESS, + id: tId, + title: options.success.title, + message: options.success.message?.(data), + }); + }) + .catch((data: ToastData) => { + setToast({ + type: TOAST_TYPE.ERROR, + id: tId, + title: options.error.title, + message: options.error.message?.(data), + }); + }); +}; diff --git a/space/components/common/index.ts b/space/components/common/index.ts index f1c0b088e..36cc3c898 100644 --- a/space/components/common/index.ts +++ b/space/components/common/index.ts @@ -1 +1,2 @@ export * from "./latest-feature-block"; +export * from "./project-logo"; diff --git a/space/components/common/project-logo.tsx b/space/components/common/project-logo.tsx new file mode 100644 index 000000000..3d5887b28 --- /dev/null +++ b/space/components/common/project-logo.tsx @@ -0,0 +1,34 @@ +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TProjectLogoProps } from "@plane/types"; + +type Props = { + className?: string; + logo: TProjectLogoProps; +}; + +export const ProjectLogo: React.FC = (props) => { + const { className, logo } = props; + + if (logo.in_use === "icon" && logo.icon) + return ( + + {logo.icon.name} + + ); + + if (logo.in_use === "emoji" && logo.emoji) + return ( + + {logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))} + + ); + + return ; +}; diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx index 0bc493b16..feb11ed13 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/index.tsx @@ -1,15 +1,12 @@ import { useEffect } from "react"; - import Link from "next/link"; import { useRouter } from "next/router"; - -// mobx import { observer } from "mobx-react-lite"; // components -// import { NavbarSearch } from "./search"; import { NavbarIssueBoardView } from "./issue-board-view"; import { NavbarTheme } from "./theme"; import { IssueFiltersDropdown } from "components/issues/filters"; +import { ProjectLogo } from "components/common"; // ui import { Avatar, Button } from "@plane/ui"; import { Briefcase } from "lucide-react"; @@ -19,18 +16,6 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; import { TIssueBoardKeys } from "types/issue"; -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 { project: projectStore, @@ -123,27 +108,15 @@ const IssueNavbar = observer(() => {
{/* project detail */}
-
- {projectStore.project ? ( - projectStore.project?.emoji ? ( - - {renderEmoji(projectStore.project.emoji)} - - ) : projectStore.project?.icon_prop ? ( -
- {renderEmoji(projectStore.project.icon_prop)} -
- ) : ( - - {projectStore.project?.name.charAt(0)} - - ) - ) : ( - - - - )} -
+ {projectStore.project ? ( + + + + ) : ( + + + + )}
{projectStore?.project?.name || `...`}
diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index ef1a115d2..3dba8b29c 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -93,7 +93,7 @@ export const AddComment: React.FC = observer((props) => { customClassName="p-2" editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} - onChange={(comment_json: Object, comment_html: string) => { + onChange={(comment_json: unknown, comment_html: string) => { onChange(comment_html); }} submitButton={ diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index 7c6abe199..c3a26f83e 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -115,7 +115,7 @@ export const CommentCard: React.FC = observer((props) => { value={value} debouncedUpdatesEnabled={false} customClassName="min-h-[50px] p-3 shadow-sm" - onChange={(comment_json: Object, comment_html: string) => { + onChange={(comment_json: unknown, comment_html: string) => { onChange(comment_html); }} /> diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx index a6dcedf08..0c327ca59 100644 --- a/space/components/issues/peek-overview/issue-properties.tsx +++ b/space/components/issues/peek-overview/issue-properties.tsx @@ -94,7 +94,7 @@ export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mod > {priority && ( - + )} {priority?.title ?? "None"} diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx index 5a4144db3..602277f3e 100644 --- a/space/components/issues/peek-overview/layout.tsx +++ b/space/components/issues/peek-overview/layout.tsx @@ -11,9 +11,7 @@ import { FullScreenPeekView, SidePeekView } from "components/issues/peek-overvie // lib import { useMobxStore } from "lib/mobx/store-provider"; -type Props = {}; - -export const IssuePeekOverview: React.FC = observer(() => { +export const IssuePeekOverview: React.FC = observer(() => { // states const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); const [isModalPeekOpen, setIsModalPeekOpen] = useState(false); diff --git a/space/components/ui/dropdown.tsx b/space/components/ui/dropdown.tsx index 09d27da42..75399619b 100644 --- a/space/components/ui/dropdown.tsx +++ b/space/components/ui/dropdown.tsx @@ -67,13 +67,13 @@ const DropdownList: React.FC = (props) => { const DropdownItem: React.FC = (props) => { const { item } = props; - const { display, children, as: as_, href, onClick, isSelected } = item; + const { display, children, as: itemAs, href, onClick, isSelected } = item; const [open, setOpen] = useState(false); return (
- {(!as_ || as_ === "button" || as_ === "div") && ( + {(!itemAs || itemAs === "button" || itemAs === "div") && ( )} - {as_ === "link" && {display}} + {itemAs === "link" && {display}} {children && setOpen(false)} items={children} />}
diff --git a/space/lib/mobx/store-provider.tsx b/space/lib/mobx/store-provider.tsx index c6fde14ae..e12f2823a 100644 --- a/space/lib/mobx/store-provider.tsx +++ b/space/lib/mobx/store-provider.tsx @@ -9,10 +9,10 @@ let rootStore: RootStore = new RootStore(); export const MobxStoreContext = createContext(rootStore); const initializeStore = () => { - const _rootStore: RootStore = rootStore ?? new RootStore(); - if (typeof window === "undefined") return _rootStore; - if (!rootStore) rootStore = _rootStore; - return _rootStore; + const singletonRootStore: RootStore = rootStore ?? new RootStore(); + if (typeof window === "undefined") return singletonRootStore; + if (!rootStore) rootStore = singletonRootStore; + return singletonRootStore; }; export const MobxStoreProvider = ({ children }: any) => { diff --git a/space/package.json b/space/package.json index a1d600a60..4951d5e30 100644 --- a/space/package.json +++ b/space/package.json @@ -20,6 +20,7 @@ "@plane/document-editor": "*", "@plane/lite-text-editor": "*", "@plane/rich-text-editor": "*", + "@plane/types": "*", "@plane/ui": "*", "@sentry/nextjs": "^7.85.0", "axios": "^1.3.4", @@ -49,9 +50,7 @@ "@types/react-dom": "^18.2.17", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.48.2", - "eslint": "8.34.0", "eslint-config-custom": "*", - "eslint-config-next": "13.2.1", "tailwind-config-custom": "*", "tsconfig": "*" } diff --git a/space/types/project.ts b/space/types/project.ts index e0e1bba9e..7e81d366c 100644 --- a/space/types/project.ts +++ b/space/types/project.ts @@ -1,3 +1,5 @@ +import { TProjectLogoProps } from "@plane/types"; + export interface IWorkspace { id: string; name: string; @@ -9,10 +11,8 @@ export interface IProject { identifier: string; name: string; description: string; - icon: string; cover_image: string | null; - icon_prop: string | null; - emoji: string | null; + logo_props: TProjectLogoProps; } export interface IProjectSettings { diff --git a/turbo.json b/turbo.json index bd5ee34b5..9302a7183 100644 --- a/turbo.json +++ b/turbo.json @@ -16,6 +16,7 @@ "NEXT_PUBLIC_DEPLOY_WITH_NGINX", "NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_HOST", + "NEXT_PUBLIC_POSTHOG_DEBUG", "JITSU_TRACKER_ACCESS_KEY", "JITSU_TRACKER_HOST" ], diff --git a/web/.eslintrc.js b/web/.eslintrc.js index c8df60750..eb05b2af8 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -1,4 +1,103 @@ module.exports = { root: true, extends: ["custom"], + parser: "@typescript-eslint/parser", + settings: { + "import/resolver": { + typescript: {}, + node: { + moduleDirectory: ["node_modules", "."], + }, + }, + }, + rules: { + // "import/order": [ + // "error", + // { + // groups: ["builtin", "external", "internal", "parent", "sibling"], + // pathGroups: [ + // { + // pattern: "react", + // group: "external", + // position: "before", + // }, + // { + // pattern: "@headlessui/**", + // group: "external", + // position: "after", + // }, + // { + // pattern: "lucide-react", + // group: "external", + // position: "after", + // }, + // { + // pattern: "@plane/ui", + // group: "external", + // position: "after", + // }, + // { + // pattern: "components/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "constants/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "contexts/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "helpers/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "hooks/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "layouts/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "lib/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "services/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "store/**", + // group: "internal", + // position: "before", + // }, + // { + // pattern: "@plane/types", + // group: "internal", + // position: "after", + // }, + // { + // pattern: "lib/types", + // group: "internal", + // position: "after", + // }, + // ], + // pathGroupsExcludedImportTypes: ["builtin", "internal", "react"], + // alphabetize: { + // order: "asc", + // caseInsensitive: true, + // }, + // }, + // ], + }, }; diff --git a/web/components/account/deactivate-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx index 701db6ad9..34129cebe 100644 --- a/web/components/account/deactivate-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -1,15 +1,13 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; +import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; -import { mutate } from "swr"; // hooks -import { useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { useUser } from "hooks/store"; type Props = { isOpen: boolean; @@ -26,7 +24,6 @@ export const DeactivateAccountModal: React.FC = (props) => { const router = useRouter(); - const { setToastAlert } = useToast(); const { setTheme } = useTheme(); const handleClose = () => { @@ -39,8 +36,8 @@ export const DeactivateAccountModal: React.FC = (props) => { await deactivateAccount() .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Account deactivated successfully.", }); @@ -50,8 +47,8 @@ export const DeactivateAccountModal: React.FC = (props) => { handleClose(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error, }) @@ -89,8 +86,11 @@ export const DeactivateAccountModal: React.FC = (props) => {
-
-
diff --git a/web/components/account/sign-up-forms/email.tsx b/web/components/account/sign-up-forms/email.tsx index 0d5861b4e..22dba892f 100644 --- a/web/components/account/sign-up-forms/email.tsx +++ b/web/components/account/sign-up-forms/email.tsx @@ -1,15 +1,13 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; -import { observer } from "mobx-react-lite"; // services -import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; -// ui -import { Button, Input } from "@plane/ui"; -// helpers +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; import { checkEmailValidity } from "helpers/string.helper"; +import { AuthService } from "services/auth.service"; +// ui +// helpers // types import { IEmailCheckData } from "@plane/types"; @@ -27,7 +25,6 @@ const authService = new AuthService(); export const SignUpEmailForm: React.FC = observer((props) => { const { onSubmit, updateEmail } = props; // hooks - const { setToastAlert } = useToast(); const { control, formState: { errors, isSubmitting, isValid }, @@ -52,8 +49,8 @@ export const SignUpEmailForm: React.FC = observer((props) => { .emailCheck(payload) .then(() => onSubmit()) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/account/sign-up-forms/optional-set-password.tsx b/web/components/account/sign-up-forms/optional-set-password.tsx index b49adabbb..93f774248 100644 --- a/web/components/account/sign-up-forms/optional-set-password.tsx +++ b/web/components/account/sign-up-forms/optional-set-password.tsx @@ -1,19 +1,19 @@ import React, { useState } from "react"; import { Controller, useForm } from "react-hook-form"; // services +import { Eye, EyeOff } from "lucide-react"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { ESignUpSteps } from "components/account"; +import { PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useEventTracker } from "hooks/store"; import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; -import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; +// components // constants -import { ESignUpSteps } from "components/account"; -import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker"; // icons -import { Eye, EyeOff } from "lucide-react"; type Props = { email: string; @@ -41,8 +41,6 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { const [showPassword, setShowPassword] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -65,8 +63,8 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { await authService .setPassword(payload) .then(async () => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Password created successfully.", }); @@ -81,8 +79,8 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { state: "FAILED", first_time: true, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); @@ -164,7 +162,7 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => {
)} /> -

+

This password will continue to be your account{"'"}s password.

diff --git a/web/components/account/sign-up-forms/password.tsx b/web/components/account/sign-up-forms/password.tsx index 293e03ef8..7fab81fbe 100644 --- a/web/components/account/sign-up-forms/password.tsx +++ b/web/components/account/sign-up-forms/password.tsx @@ -1,16 +1,14 @@ import React, { useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff, XCircle } from "lucide-react"; // services -import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; +import { AuthService } from "services/auth.service"; // types import { IPasswordSignInData } from "@plane/types"; @@ -34,8 +32,6 @@ export const SignUpPasswordForm: React.FC = observer((props) => { const { onSubmit } = props; // states const [showPassword, setShowPassword] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -59,8 +55,8 @@ export const SignUpPasswordForm: React.FC = observer((props) => { .passwordSignIn(payload) .then(async () => await onSubmit()) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) @@ -138,7 +134,7 @@ export const SignUpPasswordForm: React.FC = observer((props) => {
)} /> -

+

This password will continue to be your account{"'"}s password.

diff --git a/web/components/account/sign-up-forms/root.tsx b/web/components/account/sign-up-forms/root.tsx index 8eeb5e99f..455112e9e 100644 --- a/web/components/account/sign-up-forms/root.tsx +++ b/web/components/account/sign-up-forms/root.tsx @@ -1,9 +1,7 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useApplication, useEventTracker } from "hooks/store"; -import useSignInRedirection from "hooks/use-sign-in-redirection"; -// components +import Link from "next/link"; import { OAuthOptions, SignUpEmailForm, @@ -11,9 +9,11 @@ import { SignUpPasswordForm, SignUpUniqueCodeForm, } from "components/account"; -import Link from "next/link"; -// constants import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker"; +import { useApplication, useEventTracker } from "hooks/store"; +import useSignInRedirection from "hooks/use-sign-in-redirection"; +// components +// constants export enum ESignUpSteps { EMAIL = "EMAIL", diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx index 1b54ef9eb..82f9685b1 100644 --- a/web/components/account/sign-up-forms/unique-code.tsx +++ b/web/components/account/sign-up-forms/unique-code.tsx @@ -3,20 +3,20 @@ import Link from "next/link"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; // services +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; + +import { CODE_VERIFIED } from "constants/event-tracker"; +import { checkEmailValidity } from "helpers/string.helper"; +import { useEventTracker } from "hooks/store"; +import useTimer from "hooks/use-timer"; import { AuthService } from "services/auth.service"; import { UserService } from "services/user.service"; // hooks -import useToast from "hooks/use-toast"; -import useTimer from "hooks/use-timer"; -import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; // helpers -import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData, IMagicSignInData } from "@plane/types"; // constants -import { CODE_VERIFIED } from "constants/event-tracker"; type Props = { email: string; @@ -44,8 +44,6 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // timer const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30); // form info @@ -84,8 +82,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { captureEvent(CODE_VERIFIED, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); @@ -101,8 +99,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { .generateUniqueCode(payload) .then(() => { setResendCodeTimer(30); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "A new unique code has been sent to your email.", }); @@ -112,8 +110,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { }); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) @@ -204,8 +202,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { {resendTimerCode > 0 ? `Request new code in ${resendTimerCode}s` : isRequestingNewCode - ? "Requesting new code" - : "Request new code"} + ? "Requesting new code" + : "Request new code"}
diff --git a/web/components/analytics/custom-analytics/custom-analytics.tsx b/web/components/analytics/custom-analytics/custom-analytics.tsx index 0c3ec8925..1159689c6 100644 --- a/web/components/analytics/custom-analytics/custom-analytics.tsx +++ b/web/components/analytics/custom-analytics/custom-analytics.tsx @@ -1,17 +1,17 @@ -import { useRouter } from "next/router"; -import useSWR from "swr"; -import { useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import { useForm } from "react-hook-form"; +import useSWR from "swr"; // services -import { AnalyticsService } from "services/analytics.service"; // components import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics"; // types -import { IAnalyticsParams } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; import { useApplication } from "hooks/store"; +import { AnalyticsService } from "services/analytics.service"; +import { IAnalyticsParams } from "@plane/types"; type Props = { additionalParams?: Partial; diff --git a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx index ec7c40195..b90e9994f 100644 --- a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx +++ b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx @@ -60,8 +60,8 @@ export const CustomTooltip: React.FC = ({ datum, analytics, params }) => ? "capitalize" : "" : params.x_axis === "priority" || params.x_axis === "state__group" - ? "capitalize" - : "" + ? "capitalize" + : "" }`} > {params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}: diff --git a/web/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx index 51b4089c4..0e70fd898 100644 --- a/web/components/analytics/custom-analytics/graph/index.tsx +++ b/web/components/analytics/custom-analytics/graph/index.tsx @@ -1,15 +1,15 @@ // nivo import { BarDatum } from "@nivo/bar"; // components -import { CustomTooltip } from "./custom-tooltip"; import { Tooltip } from "@plane/ui"; // ui import { BarGraph } from "components/ui"; // helpers -import { findStringWithMostCharacters } from "helpers/array.helper"; import { generateBarColor, generateDisplayName } from "helpers/analytics.helper"; +import { findStringWithMostCharacters } from "helpers/array.helper"; // types import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; +import { CustomTooltip } from "./custom-tooltip"; type Props = { analytics: IAnalyticsResponse; @@ -101,8 +101,8 @@ export const AnalyticsGraph: React.FC = ({ analytics, barGraphData, param ? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase() : "?" : datum.value && datum.value !== "None" - ? `${datum.value}`.toUpperCase()[0] - : "?"} + ? `${datum.value}`.toUpperCase()[0] + : "?"} diff --git a/web/components/analytics/custom-analytics/main-content.tsx b/web/components/analytics/custom-analytics/main-content.tsx index 3c199f807..e13b9cdd1 100644 --- a/web/components/analytics/custom-analytics/main-content.tsx +++ b/web/components/analytics/custom-analytics/main-content.tsx @@ -2,15 +2,15 @@ import { useRouter } from "next/router"; import { mutate } from "swr"; // components +import { Button, Loader } from "@plane/ui"; import { AnalyticsGraph, AnalyticsTable } from "components/analytics"; // ui -import { Button, Loader } from "@plane/ui"; // helpers +import { ANALYTICS } from "constants/fetch-keys"; import { convertResponseToBarGraphData } from "helpers/analytics.helper"; // types import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; // fetch-keys -import { ANALYTICS } from "constants/fetch-keys"; type Props = { analytics: IAnalyticsResponse | undefined; @@ -33,7 +33,7 @@ export const CustomAnalyticsMainContent: React.FC = (props) => { {!error ? ( analytics ? ( analytics.total > 0 ? ( -
+
= observer((props) => { return (
{!isProjectLevel && (
diff --git a/web/components/analytics/custom-analytics/select/project.tsx b/web/components/analytics/custom-analytics/select/project.tsx index 3c08e1574..61c3acb09 100644 --- a/web/components/analytics/custom-analytics/select/project.tsx +++ b/web/components/analytics/custom-analytics/select/project.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // hooks +import { CustomSearchSelect } from "@plane/ui"; import { useProject } from "hooks/store"; // ui -import { CustomSearchSelect } from "@plane/ui"; type Props = { value: string[] | undefined; diff --git a/web/components/analytics/custom-analytics/select/segment.tsx b/web/components/analytics/custom-analytics/select/segment.tsx index 055665d9e..de94eac62 100644 --- a/web/components/analytics/custom-analytics/select/segment.tsx +++ b/web/components/analytics/custom-analytics/select/segment.tsx @@ -3,9 +3,9 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types +import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants -import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; type Props = { value: TXAxisValues | null | undefined; diff --git a/web/components/analytics/custom-analytics/select/x-axis.tsx b/web/components/analytics/custom-analytics/select/x-axis.tsx index 74ee99a77..9daecaaa0 100644 --- a/web/components/analytics/custom-analytics/select/x-axis.tsx +++ b/web/components/analytics/custom-analytics/select/x-axis.tsx @@ -3,9 +3,9 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types +import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants -import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; type Props = { value: TXAxisValues; diff --git a/web/components/analytics/custom-analytics/select/y-axis.tsx b/web/components/analytics/custom-analytics/select/y-axis.tsx index 9f66c6b54..92e4fd2e5 100644 --- a/web/components/analytics/custom-analytics/select/y-axis.tsx +++ b/web/components/analytics/custom-analytics/select/y-axis.tsx @@ -1,9 +1,9 @@ // ui import { CustomSelect } from "@plane/ui"; // types +import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; import { TYAxisValues } from "@plane/types"; // constants -import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; type Props = { value: TYAxisValues; diff --git a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx index f7ba07b75..31812cb00 100644 --- a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx +++ b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react-lite"; // hooks -import { useProject } from "hooks/store"; // icons import { Contrast, LayoutGrid, Users } from "lucide-react"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; +import { useProject } from "hooks/store"; +import { ProjectLogo } from "components/project"; type Props = { projectIds: string[]; @@ -19,7 +19,7 @@ export const CustomAnalyticsSidebarProjectsList: React.FC = observer((pro return (

Selected Projects

-
+
{projectIds.map((projectId) => { const project = getProjectById(projectId); @@ -28,21 +28,15 @@ export const CustomAnalyticsSidebarProjectsList: React.FC = observer((pro return (
- {project.emoji ? ( - {renderEmoji(project.emoji)} - ) : project.icon_prop ? ( -
{renderEmoji(project.icon_prop)}
- ) : ( - - {project?.name.charAt(0)} - - )} +
+ +

{truncateText(project.name, 20)}

({project.identifier})
-
+
diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index 6a7b3c7b9..26f97e8f9 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -1,12 +1,13 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useCycle, useMember, useModule, useProject } from "hooks/store"; -// helpers -import { renderEmoji } from "helpers/emoji.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; -// constants import { NETWORK_CHOICES } from "constants/project"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { useCycle, useMember, useModule, useProject } from "hooks/store"; +// components +import { ProjectLogo } from "components/project"; +// helpers +// constants export const CustomAnalyticsSidebarHeader = observer(() => { const router = useRouter(); @@ -81,15 +82,9 @@ export const CustomAnalyticsSidebarHeader = observer(() => { ) : (
- {projectDetails?.emoji ? ( -
{renderEmoji(projectDetails.emoji)}
- ) : projectDetails?.icon_prop ? ( -
- {renderEmoji(projectDetails.icon_prop)} -
- ) : ( - - {projectDetails?.name.charAt(0)} + {projectDetails && ( + + )}

{projectDetails?.name}

diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index 3ad2805f2..a48ea3c03 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -1,25 +1,24 @@ -import { useEffect, } from "react"; -import { useRouter } from "next/router"; +import { useEffect } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { mutate } from "swr"; // services -import { AnalyticsService } from "services/analytics.service"; // hooks -import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // components -import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // ui -import { Button, LayersIcon } from "@plane/ui"; -// icons import { CalendarDays, Download, RefreshCw } from "lucide-react"; +import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; +// icons +import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; import { cn } from "helpers/common.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { useCycle, useModule, useProject, useUser, useWorkspace } from "hooks/store"; +import { AnalyticsService } from "services/analytics.service"; +import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types"; type Props = { analytics: IAnalyticsResponse | undefined; @@ -34,8 +33,6 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { currentUser } = useUser(); const { workspaceProjectIds, getProjectById } = useProject(); @@ -107,8 +104,8 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { analyticsService .exportAnalytics(workspaceSlug.toString(), data) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: res.message, }); @@ -116,8 +113,8 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { trackExportAnalytics(); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "There was some error in exporting the analytics. Please try again.", }) @@ -146,7 +143,7 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { return (
@@ -163,8 +160,8 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { (cycleId ? cycleDetails?.created_at : moduleId - ? moduleDetails?.created_at - : projectDetails?.created_at) ?? "" + ? moduleDetails?.created_at + : projectDetails?.created_at) ?? "" )}
)} @@ -179,10 +176,10 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => {
-
+
diff --git a/web/components/api-token/modal/generated-token-details.tsx b/web/components/api-token/modal/generated-token-details.tsx index f28ea3481..fcae6b249 100644 --- a/web/components/api-token/modal/generated-token-details.tsx +++ b/web/components/api-token/modal/generated-token-details.tsx @@ -1,8 +1,6 @@ import { Copy } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, Tooltip } from "@plane/ui"; +import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; @@ -17,12 +15,10 @@ type Props = { export const GeneratedTokenDetails: React.FC = (props) => { const { handleClose, tokenDetails } = props; - const { setToastAlert } = useToast(); - const copyApiToken = (token: string) => { copyTextToClipboard(token).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Token copied to clipboard.", }) diff --git a/web/components/api-token/token-list-item.tsx b/web/components/api-token/token-list-item.tsx index 2de731222..88af9a0a2 100644 --- a/web/components/api-token/token-list-item.tsx +++ b/web/components/api-token/token-list-item.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; import { XCircle } from "lucide-react"; // components +import { Tooltip } from "@plane/ui"; import { DeleteApiTokenModal } from "components/api-token"; // ui -import { Tooltip } from "@plane/ui"; // helpers import { renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; // types diff --git a/web/components/auth-screens/not-authorized-view.tsx b/web/components/auth-screens/not-authorized-view.tsx index 8d9d6ecd4..4acec4104 100644 --- a/web/components/auth-screens/not-authorized-view.tsx +++ b/web/components/auth-screens/not-authorized-view.tsx @@ -1,8 +1,8 @@ import React from "react"; -import Link from "next/link"; -import Image from "next/image"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; // hooks import { useUser } from "hooks/store"; // layouts diff --git a/web/components/auth-screens/project/join-project.tsx b/web/components/auth-screens/project/join-project.tsx index 35b0b9b49..d35aad657 100644 --- a/web/components/auth-screens/project/join-project.tsx +++ b/web/components/auth-screens/project/join-project.tsx @@ -2,11 +2,11 @@ import { useState } from "react"; import Image from "next/image"; import { useRouter } from "next/router"; // hooks +import { ClipboardList } from "lucide-react"; +import { Button } from "@plane/ui"; import { useProject, useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // icons -import { ClipboardList } from "lucide-react"; // images import JoinProjectImg from "public/auth/project-not-authorized.svg"; diff --git a/web/components/auth-screens/workspace/not-a-member.tsx b/web/components/auth-screens/workspace/not-a-member.tsx index 502f06115..5f70e36dd 100644 --- a/web/components/auth-screens/workspace/not-a-member.tsx +++ b/web/components/auth-screens/workspace/not-a-member.tsx @@ -1,9 +1,9 @@ import Link from "next/link"; // layouts +import { Button } from "@plane/ui"; import DefaultLayout from "layouts/default-layout"; // ui -import { Button } from "@plane/ui"; export const NotAWorkspaceMember = () => ( diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index d871b64d0..e3ad89e90 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -1,14 +1,14 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useProject, useUser } from "hooks/store"; // component +import { ArchiveRestore } from "lucide-react"; import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui"; import { SelectMonthModal } from "components/automation"; // icon -import { ArchiveRestore } from "lucide-react"; // constants import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "constants/project"; +import { useProject, useUser } from "hooks/store"; // types import { IProject } from "@plane/types"; diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index 2ae4d1f9c..000f0bbf6 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -1,16 +1,16 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { ArchiveX } from "lucide-react"; +import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui"; +import { SelectMonthModal } from "components/automation"; +import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "constants/project"; import { useProject, useProjectState, useUser } from "hooks/store"; // component -import { SelectMonthModal } from "components/automation"; -import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui"; // icons -import { ArchiveX } from "lucide-react"; // types import { IProject } from "@plane/types"; // constants -import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "constants/project"; type Props = { handleChange: (formData: Partial) => Promise; diff --git a/web/components/breadcrumbs/index.tsx b/web/components/breadcrumbs/index.tsx index 16fa1e333..de93cdec3 100644 --- a/web/components/breadcrumbs/index.tsx +++ b/web/components/breadcrumbs/index.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // icons import { MoveLeft } from "lucide-react"; diff --git a/web/components/command-palette/actions/help-actions.tsx b/web/components/command-palette/actions/help-actions.tsx index 4aaaab33a..34317846a 100644 --- a/web/components/command-palette/actions/help-actions.tsx +++ b/web/components/command-palette/actions/help-actions.tsx @@ -1,9 +1,9 @@ import { Command } from "cmdk"; import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react"; // hooks +import { DiscordIcon } from "@plane/ui"; import { useApplication } from "hooks/store"; // ui -import { DiscordIcon } from "@plane/ui"; type Props = { closePalette: () => void; diff --git a/web/components/command-palette/actions/issue-actions/actions-list.tsx b/web/components/command-palette/actions/issue-actions/actions-list.tsx index 55f72c85d..98059af39 100644 --- a/web/components/command-palette/actions/issue-actions/actions-list.tsx +++ b/web/components/command-palette/actions/issue-actions/actions-list.tsx @@ -1,18 +1,16 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react"; // hooks -import { useApplication, useUser, useIssues } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; -// ui -import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; -// helpers +import { DoubleCircleIcon, UserGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; +import { EIssuesStoreType } from "constants/issue"; import { copyTextToClipboard } from "helpers/string.helper"; +import { useApplication, useUser, useIssues } from "hooks/store"; +// ui +// helpers // types import { TIssue } from "@plane/types"; -import { EIssuesStoreType } from "constants/issue"; type Props = { closePalette: () => void; @@ -37,8 +35,6 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { } = useApplication(); const { currentUser } = useUser(); - const { setToastAlert } = useToast(); - const handleUpdateIssue = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueDetails) return; @@ -71,14 +67,14 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { const url = new URL(window.location.href); copyTextToClipboard(url.href) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Some error occurred", }); }); diff --git a/web/components/command-palette/actions/issue-actions/change-assignee.tsx b/web/components/command-palette/actions/issue-actions/change-assignee.tsx index 96fba41f6..18b11e129 100644 --- a/web/components/command-palette/actions/issue-actions/change-assignee.tsx +++ b/web/components/command-palette/actions/issue-actions/change-assignee.tsx @@ -1,14 +1,14 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Check } from "lucide-react"; // mobx store +import { Avatar } from "@plane/ui"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues, useMember } from "hooks/store"; // ui -import { Avatar } from "@plane/ui"; // types import { TIssue } from "@plane/types"; -import { EIssuesStoreType } from "constants/issue"; type Props = { closePalette: () => void; diff --git a/web/components/command-palette/actions/issue-actions/change-priority.tsx b/web/components/command-palette/actions/issue-actions/change-priority.tsx index 8d1c48261..d07866833 100644 --- a/web/components/command-palette/actions/issue-actions/change-priority.tsx +++ b/web/components/command-palette/actions/issue-actions/change-priority.tsx @@ -1,15 +1,15 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Check } from "lucide-react"; // mobx store +import { PriorityIcon } from "@plane/ui"; +import { EIssuesStoreType, ISSUE_PRIORITIES } from "constants/issue"; import { useIssues } from "hooks/store"; // ui -import { PriorityIcon } from "@plane/ui"; // types import { TIssue, TIssuePriorities } from "@plane/types"; // constants -import { EIssuesStoreType, ISSUE_PRIORITIES } from "constants/issue"; type Props = { closePalette: () => void; diff --git a/web/components/command-palette/actions/issue-actions/change-state.tsx b/web/components/command-palette/actions/issue-actions/change-state.tsx index 7841a4a1e..d208facc9 100644 --- a/web/components/command-palette/actions/issue-actions/change-state.tsx +++ b/web/components/command-palette/actions/issue-actions/change-state.tsx @@ -1,15 +1,15 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { Check } from "lucide-react"; +import { Spinner, StateGroupIcon } from "@plane/ui"; +import { EIssuesStoreType } from "constants/issue"; import { useProjectState, useIssues } from "hooks/store"; // ui -import { Spinner, StateGroupIcon } from "@plane/ui"; // icons -import { Check } from "lucide-react"; // types import { TIssue } from "@plane/types"; -import { EIssuesStoreType } from "constants/issue"; type Props = { closePalette: () => void; diff --git a/web/components/command-palette/actions/project-actions.tsx b/web/components/command-palette/actions/project-actions.tsx index bdd08a0d8..d1589cb92 100644 --- a/web/components/command-palette/actions/project-actions.tsx +++ b/web/components/command-palette/actions/project-actions.tsx @@ -1,9 +1,9 @@ import { Command } from "cmdk"; import { ContrastIcon, FileText } from "lucide-react"; // hooks +import { DiceIcon, PhotoFilterIcon } from "@plane/ui"; import { useApplication, useEventTracker } from "hooks/store"; // ui -import { DiceIcon, PhotoFilterIcon } from "@plane/ui"; type Props = { closePalette: () => void; diff --git a/web/components/command-palette/actions/search-results.tsx b/web/components/command-palette/actions/search-results.tsx index 769a26be7..5398d889d 100644 --- a/web/components/command-palette/actions/search-results.tsx +++ b/web/components/command-palette/actions/search-results.tsx @@ -1,5 +1,5 @@ -import { useRouter } from "next/router"; import { Command } from "cmdk"; +import { useRouter } from "next/router"; // helpers import { commandGroups } from "components/command-palette"; // types diff --git a/web/components/command-palette/actions/theme-actions.tsx b/web/components/command-palette/actions/theme-actions.tsx index 976a63c87..fe4a9fa20 100644 --- a/web/components/command-palette/actions/theme-actions.tsx +++ b/web/components/command-palette/actions/theme-actions.tsx @@ -1,13 +1,14 @@ import React, { FC, useEffect, useState } from "react"; import { Command } from "cmdk"; +import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; import { Settings } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks -import { useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; -// constants +import { TOAST_TYPE, setToast } from "@plane/ui"; import { THEME_OPTIONS } from "constants/themes"; +import { useUser } from "hooks/store"; +// ui +// constants type Props = { closePalette: () => void; @@ -21,15 +22,14 @@ export const CommandPaletteThemeActions: FC = observer((props) => { const { updateCurrentUserTheme } = useUser(); // hooks const { setTheme } = useTheme(); - const { setToastAlert } = useToast(); const updateUserTheme = async (newTheme: string) => { setTheme(newTheme); return updateCurrentUserTheme(newTheme).catch(() => { - setToastAlert({ + setToast({ + type: TOAST_TYPE.ERROR, title: "Failed to save user theme settings!", - type: "error", }); }); }; diff --git a/web/components/command-palette/actions/workspace-settings-actions.tsx b/web/components/command-palette/actions/workspace-settings-actions.tsx index 1f05234f4..5a2b2cd69 100644 --- a/web/components/command-palette/actions/workspace-settings-actions.tsx +++ b/web/components/command-palette/actions/workspace-settings-actions.tsx @@ -1,10 +1,10 @@ -import { useRouter } from "next/router"; import { Command } from "cmdk"; // hooks -import { useUser } from "hooks/store"; import Link from "next/link"; +import { useRouter } from "next/router"; // constants import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; +import { useUser } from "hooks/store"; type Props = { closePalette: () => void; diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index b52976aa8..747075181 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -1,18 +1,12 @@ import React, { useEffect, useState } from "react"; +import { Command } from "cmdk"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { Command } from "cmdk"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { FolderPlus, Search, Settings } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject } from "hooks/store"; -// services -import { WorkspaceService } from "services/workspace.service"; -import { IssueService } from "services/issue"; -// hooks -import useDebounce from "hooks/use-debounce"; -// components +import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; import { CommandPaletteThemeActions, ChangeIssueAssignee, @@ -24,11 +18,17 @@ import { CommandPaletteWorkspaceSettingsActions, CommandPaletteSearchResults, } from "components/command-palette"; -import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; +import { useApplication, useEventTracker, useProject } from "hooks/store"; +// services +import useDebounce from "hooks/use-debounce"; +import { IssueService } from "services/issue"; +import { WorkspaceService } from "services/workspace.service"; +// hooks +// components // types import { IWorkspaceSearchResults } from "@plane/types"; // fetch-keys -import { ISSUE_DETAILS } from "constants/fetch-keys"; // services const workspaceService = new WorkspaceService(); diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index 396003589..ab2743afd 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -1,26 +1,28 @@ import React, { useCallback, useEffect, FC } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components +import { TOAST_TYPE, setToast } from "@plane/ui"; + import { CommandModal, ShortcutsModal } from "components/command-palette"; +// ui +// components import { BulkDeleteIssuesModal } from "components/core"; import { CycleCreateUpdateModal } from "components/cycles"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateModuleModal } from "components/modules"; +import { CreateUpdatePageModal } from "components/pages"; import { CreateProjectModal } from "components/project"; import { CreateUpdateProjectViewModal } from "components/views"; -import { CreateUpdatePageModal } from "components/pages"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; // services -import { IssueService } from "services/issue"; // fetch keys import { ISSUE_DETAILS } from "constants/fetch-keys"; import { EIssuesStoreType } from "constants/issue"; +import { copyTextToClipboard } from "helpers/string.helper"; +import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; +import { IssueService } from "services/issue"; // services const issueService = new IssueService(); @@ -63,8 +65,6 @@ export const CommandPalette: FC = observer(() => { createIssueStoreType, } = commandPalette; - const { setToastAlert } = useToast(); - const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId @@ -78,18 +78,18 @@ export const CommandPalette: FC = observer(() => { const url = new URL(window.location.href); copyTextToClipboard(url.href) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Some error occurred", }); }); - }, [setToastAlert, issueId]); + }, [issueId]); const handleKeyDown = useCallback( (e: KeyboardEvent) => { diff --git a/web/components/command-palette/helpers.tsx b/web/components/command-palette/helpers.tsx index 44fc55bbe..2d6a38c71 100644 --- a/web/components/command-palette/helpers.tsx +++ b/web/components/command-palette/helpers.tsx @@ -1,6 +1,6 @@ // types -import { ContrastIcon, DiceIcon, LayersIcon, PhotoFilterIcon } from "@plane/ui"; import { Briefcase, FileText, LayoutGrid } from "lucide-react"; +import { ContrastIcon, DiceIcon, LayersIcon, PhotoFilterIcon } from "@plane/ui"; import { IWorkspaceDefaultSearchResult, IWorkspaceIssueSearchResult, diff --git a/web/components/command-palette/shortcuts-modal/modal.tsx b/web/components/command-palette/shortcuts-modal/modal.tsx index 3054bdb28..97a9c9891 100644 --- a/web/components/command-palette/shortcuts-modal/modal.tsx +++ b/web/components/command-palette/shortcuts-modal/modal.tsx @@ -2,9 +2,9 @@ import { FC, useState, Fragment } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { Search, X } from "lucide-react"; // components +import { Input } from "@plane/ui"; import { ShortcutCommandsList } from "components/command-palette"; // ui -import { Input } from "@plane/ui"; type Props = { isOpen: boolean; diff --git a/web/components/common/breadcrumb-link.tsx b/web/components/common/breadcrumb-link.tsx index e5f1dbce6..dfa437231 100644 --- a/web/components/common/breadcrumb-link.tsx +++ b/web/components/common/breadcrumb-link.tsx @@ -1,5 +1,5 @@ -import { Tooltip } from "@plane/ui"; import Link from "next/link"; +import { Tooltip } from "@plane/ui"; type Props = { label?: string; diff --git a/web/components/common/product-updates-modal.tsx b/web/components/common/product-updates-modal.tsx index cd0a5b9ff..20b8b815e 100644 --- a/web/components/common/product-updates-modal.tsx +++ b/web/components/common/product-updates-modal.tsx @@ -3,14 +3,14 @@ import useSWR from "swr"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // services -import { WorkspaceService } from "services/workspace.service"; // components -import { MarkdownRenderer } from "components/ui"; -import { Loader } from "@plane/ui"; -// icons import { X } from "lucide-react"; +import { Loader } from "@plane/ui"; +import { MarkdownRenderer } from "components/ui"; +// icons // helpers import { renderFormattedDate } from "helpers/date-time.helper"; +import { WorkspaceService } from "services/workspace.service"; type Props = { isOpen: boolean; diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 72a67883e..020e88ccc 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -1,10 +1,8 @@ -import { useRouter } from "next/router"; import { useEffect } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // store hooks -import { useEstimate, useLabel } from "hooks/store"; // icons -import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui"; import { TagIcon, CopyPlus, @@ -20,9 +18,11 @@ import { MessageSquareIcon, UsersIcon, } from "lucide-react"; +import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; +import { useEstimate, useLabel } from "hooks/store"; // types import { IIssueActivity } from "@plane/types"; diff --git a/web/components/core/filters/date-filter-modal.tsx b/web/components/core/filters/date-filter-modal.tsx index c5238ec1c..3d7e78ba1 100644 --- a/web/components/core/filters/date-filter-modal.tsx +++ b/web/components/core/filters/date-filter-modal.tsx @@ -1,14 +1,14 @@ import { Fragment } from "react"; -import { Controller, useForm } from "react-hook-form"; import { DayPicker } from "react-day-picker"; +import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { X } from "lucide-react"; // components -import { DateFilterSelect } from "./date-filter-select"; // ui import { Button } from "@plane/ui"; // helpers import { renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper"; +import { DateFilterSelect } from "./date-filter-select"; type Props = { title: string; @@ -37,7 +37,8 @@ export const DateFilterModal: React.FC = ({ title, handleClose, isOpen, o const handleFormSubmit = (formData: TFormValues) => { const { filterType, date1, date2 } = formData; - if (filterType === "range") onSelect([`${renderFormattedPayloadDate(date1)};after`, `${renderFormattedPayloadDate(date2)};before`]); + if (filterType === "range") + onSelect([`${renderFormattedPayloadDate(date1)};after`, `${renderFormattedPayloadDate(date2)};before`]); else onSelect([`${renderFormattedPayloadDate(date1)};${filterType}`]); handleClose(); @@ -92,9 +93,7 @@ export const DateFilterModal: React.FC = ({ title, handleClose, isOpen, o defaultMonth={value ? new Date(value) : undefined} onSelect={(date) => onChange(date)} mode="single" - disabled={[ - { after: new Date(watch("date2")) } - ]} + disabled={[{ after: new Date(watch("date2")) }]} className="border border-custom-border-200 p-3 rounded-md" /> )} @@ -109,9 +108,7 @@ export const DateFilterModal: React.FC = ({ title, handleClose, isOpen, o defaultMonth={value ? new Date(value) : undefined} onSelect={(date) => onChange(date)} mode="single" - disabled={[ - { before: new Date(watch("date1")) } - ]} + disabled={[{ before: new Date(watch("date1")) }]} className="border border-custom-border-200 p-3 rounded-md" /> )} diff --git a/web/components/core/filters/date-filter-select.tsx b/web/components/core/filters/date-filter-select.tsx index 9bb10f800..47207e0cc 100644 --- a/web/components/core/filters/date-filter-select.tsx +++ b/web/components/core/filters/date-filter-select.tsx @@ -1,10 +1,7 @@ import React from "react"; - +import { CalendarDays } from "lucide-react"; // ui import { CustomSelect, CalendarAfterIcon, CalendarBeforeIcon } from "@plane/ui"; -// icons -import { CalendarDays } from "lucide-react"; -// fetch-keys type Props = { title: string; @@ -22,17 +19,17 @@ const dueDateRange: DueDate[] = [ { name: "before", value: "before", - icon: , + icon: , }, { name: "after", value: "after", - icon: , + icon: , }, { name: "range", value: "range", - icon: , + icon: , }, ]; diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index b2e4c4c9f..3db409bb0 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -1,22 +1,22 @@ import React, { useEffect, useState, useRef, useCallback } from "react"; +import { observer } from "mobx-react-lite"; import Image from "next/image"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; import { useDropzone } from "react-dropzone"; -import { Tab, Popover } from "@headlessui/react"; import { Control, Controller } from "react-hook-form"; +import useSWR from "swr"; +import { Tab, Popover } from "@headlessui/react"; // hooks +import { Button, Input, Loader } from "@plane/ui"; +import { MAX_FILE_SIZE } from "constants/common"; import { useApplication, useWorkspace } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; // services +import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { FileService } from "services/file.service"; // hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { Button, Input, Loader } from "@plane/ui"; // constants -import { MAX_FILE_SIZE } from "constants/common"; const tabOptions = [ { @@ -187,7 +187,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { ); })} - + {(unsplashImages || !unsplashError) && (
diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index 39be2872b..94d665fa7 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -1,27 +1,26 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { SubmitHandler, useForm } from "react-hook-form"; +import useSWR from "swr"; import { Combobox, Dialog, Transition } from "@headlessui/react"; // services -import { IssueService } from "services/issue"; -// hooks -import useToast from "hooks/use-toast"; -// ui -import { Button, LayersIcon } from "@plane/ui"; -// icons import { Search } from "lucide-react"; +import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; + +import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import { EIssuesStoreType } from "constants/issue"; +import { useIssues, useProject } from "hooks/store"; +import { IssueService } from "services/issue"; +// ui +// icons // types import { IUser, TIssue } from "@plane/types"; // fetch keys -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; // store hooks -import { useIssues, useProject } from "hooks/store"; // components import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item"; // constants -import { EIssuesStoreType } from "constants/issue"; type FormInput = { delete_issue_ids: string[]; @@ -55,8 +54,6 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { : null ); - const { setToastAlert } = useToast(); - const { handleSubmit, watch, @@ -79,8 +76,8 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { if (!workspaceSlug || !projectId) return; if (!data.delete_issue_ids || data.delete_issue_ids.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one issue.", }); @@ -91,16 +88,16 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { await removeBulkIssues(workspaceSlug as string, projectId as string, data.delete_issue_ids) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issues deleted successfully!", }); handleClose(); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }) diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index c4fa25c6d..3e3c2871c 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -2,12 +2,12 @@ import React, { useEffect, useState } from "react"; import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Rocket, Search, X } from "lucide-react"; // services -import { ProjectService } from "services/project"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; + import useDebounce from "hooks/use-debounce"; + +import { ProjectService } from "services/project"; // ui -import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // types import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; @@ -43,8 +43,6 @@ export const ExistingIssuesListModal: React.FC = (props) => { const debouncedSearchTerm: string = useDebounce(searchTerm, 500); - const { setToastAlert } = useToast(); - const handleClose = () => { onClose(); setSearchTerm(""); @@ -54,8 +52,8 @@ export const ExistingIssuesListModal: React.FC = (props) => { const onSubmit = async () => { if (selectedIssues.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one issue.", }); @@ -68,12 +66,6 @@ export const ExistingIssuesListModal: React.FC = (props) => { await handleOnSubmit(selectedIssues).finally(() => setIsSubmitting(false)); handleClose(); - - setToastAlert({ - title: "Success", - type: "success", - message: `Issue${selectedIssues.length > 1 ? "s" : ""} added successfully`, - }); }; useEffect(() => { @@ -184,7 +176,10 @@ export const ExistingIssuesListModal: React.FC = (props) => { )}
- + {searchTerm !== "" && (
Search results for{" "} diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index 590015e12..4bee1af33 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -1,17 +1,16 @@ import React, { useEffect, useState, useRef, Fragment } from "react"; +import { Placement } from "@popperjs/core"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // services -import { AIService } from "services/ai.service"; -// hooks -import useToast from "hooks/use-toast"; import { usePopper } from "react-popper"; -// ui -import { Button, Input } from "@plane/ui"; -// components import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor"; import { Popover, Transition } from "@headlessui/react"; +// hooks +// ui +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +// components // types -import { Placement } from "@popperjs/core"; +import { AIService } from "services/ai.service"; type Props = { isOpen: boolean; @@ -44,8 +43,6 @@ export const GptAssistantPopover: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); // popper const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "auto", @@ -78,8 +75,8 @@ export const GptAssistantPopover: React.FC = (props) => { ? error || "You have reached the maximum number of requests of 50 requests per month per user." : error || "Some error occurred. Please try again."; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorMessage, }); @@ -104,8 +101,8 @@ export const GptAssistantPopover: React.FC = (props) => { }; const handleInvalidTask = () => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please enter some task to get AI assistance.", }); @@ -175,8 +172,8 @@ export const GptAssistantPopover: React.FC = (props) => { const generateResponseButtonText = isSubmitting ? "Generating response..." : response === "" - ? "Generate response" - : "Generate again"; + ? "Generate response" + : "Generate again"; return ( @@ -195,7 +192,7 @@ export const GptAssistantPopover: React.FC = (props) => { > = (props) => {
)} {response !== "" && ( -
+
Response: ${response}

`} diff --git a/web/components/core/modals/link-modal.tsx b/web/components/core/modals/link-modal.tsx index 1c1372e8d..70324b4b7 100644 --- a/web/components/core/modals/link-modal.tsx +++ b/web/components/core/modals/link-modal.tsx @@ -159,8 +159,8 @@ export const LinkModal: FC = (props) => { ? "Updating Link..." : "Update Link" : isSubmitting - ? "Adding Link..." - : "Add Link"} + ? "Adding Link..." + : "Add Link"}
diff --git a/web/components/core/modals/user-image-upload-modal.tsx b/web/components/core/modals/user-image-upload-modal.tsx index 6debc2c15..7f41b8225 100644 --- a/web/components/core/modals/user-image-upload-modal.tsx +++ b/web/components/core/modals/user-image-upload-modal.tsx @@ -3,17 +3,16 @@ import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; // hooks +import { UserCircle2 } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; + +import { MAX_FILE_SIZE } from "constants/common"; import { useApplication } from "hooks/store"; // services import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; // icons -import { UserCircle2 } from "lucide-react"; // constants -import { MAX_FILE_SIZE } from "constants/common"; type Props = { handleDelete?: () => void; @@ -32,8 +31,6 @@ export const UserImageUploadModal: React.FC = observer((props) => { // states const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); - // toast alert - const { setToastAlert } = useToast(); // store hooks const { config: { envConfig }, @@ -76,8 +73,8 @@ export const UserImageUploadModal: React.FC = observer((props) => { if (value) fileService.deleteUserFile(value); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/core/modals/workspace-image-upload-modal.tsx b/web/components/core/modals/workspace-image-upload-modal.tsx index e04ccf820..9c1a8363b 100644 --- a/web/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/components/core/modals/workspace-image-upload-modal.tsx @@ -1,20 +1,18 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; // hooks +import { UserCircle2 } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { MAX_FILE_SIZE } from "constants/common"; import { useApplication, useWorkspace } from "hooks/store"; // services import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; // icons -import { UserCircle2 } from "lucide-react"; // constants -import { MAX_FILE_SIZE } from "constants/common"; type Props = { handleRemove?: () => void; @@ -37,8 +35,6 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const { config: { envConfig }, } = useApplication(); @@ -83,8 +79,8 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { if (value && currentWorkspace) fileService.deleteFile(currentWorkspace.id, value); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/core/render-if-visible-HOC.tsx b/web/components/core/render-if-visible-HOC.tsx index 24ae19fe7..f0e9f59b4 100644 --- a/web/components/core/render-if-visible-HOC.tsx +++ b/web/components/core/render-if-visible-HOC.tsx @@ -1,10 +1,10 @@ -import { cn } from "helpers/common.helper"; import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react"; +import { cn } from "helpers/common.helper"; type Props = { defaultHeight?: string; verticalOffset?: number; - horizonatlOffset?: number; + horizontalOffset?: number; root?: MutableRefObject; children: ReactNode; as?: keyof JSX.IntrinsicElements; @@ -20,7 +20,7 @@ const RenderIfVisible: React.FC = (props) => { defaultHeight = "300px", root, verticalOffset = 50, - horizonatlOffset = 0, + horizontalOffset = 0, as = "div", children, classNames = "", @@ -52,17 +52,18 @@ const RenderIfVisible: React.FC = (props) => { }, { root: root?.current, - rootMargin: `${verticalOffset}% ${horizonatlOffset}% ${verticalOffset}% ${horizonatlOffset}%`, + rootMargin: `${verticalOffset}% ${horizontalOffset}% ${verticalOffset}% ${horizontalOffset}%`, } ); observer.observe(intersectionRef.current); return () => { if (intersectionRef.current) { + // eslint-disable-next-line react-hooks/exhaustive-deps observer.unobserve(intersectionRef.current); } }; } - }, [root?.current, intersectionRef, children, changingReference]); + }, [intersectionRef, children, changingReference, root, verticalOffset, horizontalOffset]); //Set height after render useEffect(() => { diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx index 48a5e16b7..3e068e4f0 100644 --- a/web/components/core/sidebar/links-list.tsx +++ b/web/components/core/sidebar/links-list.tsx @@ -1,16 +1,14 @@ -// ui -import { ExternalLinkIcon, Tooltip } from "@plane/ui"; +import { observer } from "mobx-react"; // icons import { Pencil, Trash2, LinkIcon } from "lucide-react"; +// ui +import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; +// hooks +import { useMember } from "hooks/store"; // types import { ILinkDetails, UserAuth } from "@plane/types"; -// hooks -import useToast from "hooks/use-toast"; -import { observer } from "mobx-react"; -import { useMeasure } from "@nivo/core"; -import { useMember } from "hooks/store"; type Props = { links: ILinkDetails[]; @@ -20,18 +18,16 @@ type Props = { }; export const LinksList: React.FC = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => { - // toast - const { setToastAlert } = useToast(); const { getUserDetails } = useMember(); const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); - setToastAlert({ - message: "The URL has been successfully copied to your clipboard", - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard", + message: "The URL has been successfully copied to your clipboard", }); }; diff --git a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx index cb433de05..880cf8146 100644 --- a/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx +++ b/web/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx @@ -1,21 +1,21 @@ import { FC } from "react"; +import { observer } from "mobx-react"; import { Menu } from "lucide-react"; import { useApplication } from "hooks/store"; -import { observer } from "mobx-react"; type Props = { onClick?: () => void; -} +}; export const SidebarHamburgerToggle: FC = observer((props) => { - const { onClick } = props + const { onClick } = props; const { theme: themeStore } = useApplication(); return (
{ - if (onClick) onClick() - else themeStore.toggleMobileSidebar() + if (onClick) onClick(); + else themeStore.toggleSidebar(); }} > diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 12c387f47..6ff3d3f1e 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -4,14 +4,14 @@ import Image from "next/image"; // headless ui import { Tab } from "@headlessui/react"; // hooks +import { Avatar, StateGroupIcon } from "@plane/ui"; +import { SingleProgressStats } from "components/core"; import useLocalStorage from "hooks/use-local-storage"; // images import emptyLabel from "public/empty-state/empty_label.svg"; import emptyMembers from "public/empty-state/empty_members.svg"; // components -import { SingleProgressStats } from "components/core"; // ui -import { Avatar, StateGroupIcon } from "@plane/ui"; // types import { IModule, @@ -137,8 +137,8 @@ export const SidebarProgressStats: React.FC = ({ key={assignee.assignee_id} title={
- - {assignee.display_name} + + {assignee?.display_name ?? ""}
} completed={assignee.completed_issues} diff --git a/web/components/core/theme/color-picker-input.tsx b/web/components/core/theme/color-picker-input.tsx index 19cd519cb..03ac06eae 100644 --- a/web/components/core/theme/color-picker-input.tsx +++ b/web/components/core/theme/color-picker-input.tsx @@ -1,5 +1,6 @@ import { FC, Fragment } from "react"; // react-form +import { ColorResult, SketchPicker } from "react-color"; import { Control, Controller, @@ -11,12 +12,11 @@ import { UseFormWatch, } from "react-hook-form"; // react-color -import { ColorResult, SketchPicker } from "react-color"; // component import { Popover, Transition } from "@headlessui/react"; +import { Palette } from "lucide-react"; import { Input } from "@plane/ui"; // icons -import { Palette } from "lucide-react"; // types import { IUserTheme } from "@plane/types"; diff --git a/web/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index fdb7a6483..b9e94a2d2 100644 --- a/web/components/core/theme/custom-theme-selector.tsx +++ b/web/components/core/theme/custom-theme-selector.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; -import { Controller, useForm } from "react-hook-form"; import { useTheme } from "next-themes"; +import { Controller, useForm } from "react-hook-form"; // hooks +import { Button, InputColorPicker } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { Button, InputColorPicker } from "@plane/ui"; // types import { IUserTheme } from "@plane/types"; diff --git a/web/components/core/theme/theme-switch.tsx b/web/components/core/theme/theme-switch.tsx index bcd847a28..428e6930b 100644 --- a/web/components/core/theme/theme-switch.tsx +++ b/web/components/core/theme/theme-switch.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // constants +import { CustomSelect } from "@plane/ui"; import { THEME_OPTIONS, I_THEME_OPTION } from "constants/themes"; // ui -import { CustomSelect } from "@plane/ui"; type Props = { value: I_THEME_OPTION | null; diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 1fae0412f..a6457ab3c 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -1,11 +1,9 @@ import { MouseEvent } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import useSWR from "swr"; -import { useTheme } from "next-themes"; // hooks -import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +import { useCycle, useIssues, useMember, useProject } from "hooks/store"; // ui import { SingleProgressStats } from "components/core"; import { @@ -18,12 +16,13 @@ import { PriorityIcon, Avatar, CycleGroupIcon, + setPromiseToast, } from "@plane/ui"; // components import ProgressChart from "components/core/sidebar/progress-chart"; import { ActiveCycleProgressStats } from "components/cycles"; import { StateDropdown } from "components/dropdowns"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // icons import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react"; // helpers @@ -35,7 +34,7 @@ import { ICycle, TCycleGroups } from "@plane/types"; import { EIssuesStoreType } from "constants/issue"; import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; interface IActiveCycleDetails { workspaceSlug: string; @@ -45,9 +44,6 @@ interface IActiveCycleDetails { export const ActiveCycleDetails: React.FC = observer((props) => { // props const { workspaceSlug, projectId } = props; - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); const { issues: { fetchActiveCycleIssues }, } = useIssues(EIssuesStoreType.CYCLE); @@ -60,8 +56,6 @@ export const ActiveCycleDetails: React.FC = observer((props } = useCycle(); const { currentProjectDetails } = useProject(); const { getUserDetails } = useMember(); - // toast alert - const { setToastAlert } = useToast(); const { isLoading } = useSWR( workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null, @@ -80,11 +74,6 @@ export const ActiveCycleDetails: React.FC = observer((props : null ); - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", "active", isLightMode); - if (!activeCycle && isLoading) return ( @@ -92,15 +81,7 @@ export const ActiveCycleDetails: React.FC = observer((props ); - if (!activeCycle) - return ( - - ); + if (!activeCycle) return ; const endDate = new Date(activeCycle.end_date ?? ""); const startDate = new Date(activeCycle.start_date ?? ""); @@ -119,12 +100,18 @@ export const ActiveCycleDetails: React.FC = observer((props e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { - setToastAlert({ - type: "error", + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, }); }; @@ -132,12 +119,22 @@ export const ActiveCycleDetails: React.FC = observer((props e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { - setToastAlert({ - type: "error", + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + activeCycle.id + ); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, }); }; @@ -169,7 +166,7 @@ export const ActiveCycleDetails: React.FC = observer((props - + {`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`} {activeCycle.is_favorite ? ( @@ -289,9 +286,9 @@ export const ActiveCycleDetails: React.FC = observer((props
-
+
High Priority Issues
-
+
{activeCycleIssues ? ( activeCycleIssues.length > 0 ? ( activeCycleIssues.map((issue: any) => ( @@ -311,21 +308,21 @@ export const ActiveCycleDetails: React.FC = observer((props {currentProjectDetails?.identifier}-{issue.sequence_id} - + {truncateText(issue.name, 30)}
-
+
{}} projectId={projectId?.toString() ?? ""} - disabled={true} + disabled buttonVariant="background-with-text" /> {issue.target_date && ( -
+
{renderFormattedDateWithoutYear(issue.target_date)}
@@ -335,7 +332,7 @@ export const ActiveCycleDetails: React.FC = observer((props )) ) : ( -
+
There are no high priority issues present in this cycle.
) @@ -348,7 +345,7 @@ export const ActiveCycleDetails: React.FC = observer((props )}
-
+
diff --git a/web/components/cycles/active-cycle-stats.tsx b/web/components/cycles/active-cycle-stats.tsx index 3ca5caeb2..0cf7449ae 100644 --- a/web/components/cycles/active-cycle-stats.tsx +++ b/web/components/cycles/active-cycle-stats.tsx @@ -1,11 +1,11 @@ import React, { Fragment } from "react"; import { Tab } from "@headlessui/react"; // hooks +import { Avatar } from "@plane/ui"; +import { SingleProgressStats } from "components/core"; import useLocalStorage from "hooks/use-local-storage"; // components -import { SingleProgressStats } from "components/core"; // ui -import { Avatar } from "@plane/ui"; // types import { ICycle } from "@plane/types"; @@ -82,7 +82,7 @@ export const ActiveCycleProgressStats: React.FC = ({ cycle }) => {
- {assignee.display_name} + {assignee?.display_name ?? ""}
} completed={assignee.completed_issues} diff --git a/web/components/cycles/cycle-mobile-header.tsx b/web/components/cycles/cycle-mobile-header.tsx index b893bf993..add78943c 100644 --- a/web/components/cycles/cycle-mobile-header.tsx +++ b/web/components/cycles/cycle-mobile-header.tsx @@ -1,16 +1,16 @@ import { useCallback, useState } from "react"; import router from "next/router"; //components -import { CustomMenu } from "@plane/ui"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { CustomMenu } from "@plane/ui"; // hooks -import { useIssues, useCycle, useProjectState, useLabel, useMember } from "hooks/store"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; import { ProjectAnalyticsModal } from "components/analytics"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; +import { useIssues, useCycle, useProjectState, useLabel, useMember } from "hooks/store"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const CycleMobileHeader = () => { const [analyticsModal, setAnalyticsModal] = useState(false); @@ -21,11 +21,7 @@ export const CycleMobileHeader = () => { { key: "calendar", title: "Calendar", icon: Calendar }, ]; - const { workspaceSlug, projectId, cycleId } = router.query as { - workspaceSlug: string; - projectId: string; - cycleId: string; - }; + const { workspaceSlug, projectId, cycleId } = router.query; const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; // store hooks const { @@ -35,8 +31,14 @@ export const CycleMobileHeader = () => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); + if (!workspaceSlug || !projectId || !cycleId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); @@ -49,7 +51,7 @@ export const CycleMobileHeader = () => { const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !cycleId) return; const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { @@ -61,23 +63,41 @@ export const CycleMobileHeader = () => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] ); const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); + if (!workspaceSlug || !projectId || !cycleId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + updatedDisplayFilter, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId); + if (!workspaceSlug || !projectId || !cycleId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_PROPERTIES, + property, + cycleId.toString() + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); @@ -100,6 +120,7 @@ export const CycleMobileHeader = () => { > {layouts.map((layout, index) => ( { handleLayoutChange(ISSUE_LAYOUTS[index].key); }} @@ -152,6 +173,7 @@ export const CycleMobileHeader = () => { handleDisplayFiltersUpdate={handleDisplayFilters} displayProperties={issueFilters?.displayProperties ?? {}} handleDisplayPropertiesUpdate={handleDisplayProperties} + ignoreGroupedFilters={["cycle"]} />
diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index fbfb46b50..b7e778c10 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks import { useCycle } from "hooks/store"; // components diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index 7d6b1e000..da97f2d9d 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -1,23 +1,32 @@ import { FC, MouseEvent, useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react"; +import Link from "next/link"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; // components +import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; +import { + Avatar, + AvatarGroup, + CustomMenu, + Tooltip, + LayersIcon, + CycleGroupIcon, + TOAST_TYPE, + setToast, + setPromiseToast, +} from "@plane/ui"; import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; // ui -import { Avatar, AvatarGroup, CustomMenu, Tooltip, LayersIcon, CycleGroupIcon } from "@plane/ui"; // icons -import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers +import { CYCLE_STATUS } from "constants/cycle"; +import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // constants -import { CYCLE_STATUS } from "constants/cycle"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; +import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; //.types import { TCycleGroups } from "@plane/types"; @@ -41,8 +50,6 @@ export const CyclesBoardCard: FC = observer((props) => { } = useUser(); const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle(); const { getUserDetails } = useMember(); - // toast alert - const { setToastAlert } = useToast(); // computed const cycleDetails = getCycleById(cycleId); @@ -71,8 +78,8 @@ export const CyclesBoardCard: FC = observer((props) => { ? cycleTotalIssues === 0 ? "0 Issue" : cycleTotalIssues === cycleDetails.completed_issues - ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` - : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` + ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` + : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` : "0 Issue"; const handleCopyText = (e: MouseEvent) => { @@ -81,8 +88,8 @@ export const CyclesBoardCard: FC = observer((props) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Cycle link copied to clipboard.", }); @@ -93,42 +100,56 @@ export const CyclesBoardCard: FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then( + () => { captureEvent(CYCLE_FAVORITED, { cycle_id: cycleId, element: "Grid layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { - captureEvent(CYCLE_UNFAVORITED, { - cycle_id: cycleId, - element: "Grid layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + cycleId + ).then(() => { + captureEvent(CYCLE_UNFAVORITED, { + cycle_id: cycleId, + element: "Grid layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, + }); }; const handleEditCycle = (e: MouseEvent) => { diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 1a9069267..278d55071 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -1,13 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -// hooks -import { useUser } from "hooks/store"; // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // constants -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; export interface ICyclesBoard { cycleIds: string[]; @@ -19,15 +16,6 @@ export interface ICyclesBoard { export const CyclesBoard: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); - - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> @@ -52,12 +40,7 @@ export const CyclesBoard: FC = observer((props) => {
) : ( - + )} ); diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 31958cd84..9bf1866ff 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -1,25 +1,34 @@ import { FC, MouseEvent, useState } from "react"; +import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react"; // hooks -import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components -import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; -// ui -import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar } from "@plane/ui"; -// icons import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; -// helpers +import { + CustomMenu, + Tooltip, + CircularProgressIndicator, + CycleGroupIcon, + AvatarGroup, + Avatar, + TOAST_TYPE, + setToast, + setPromiseToast, +} from "@plane/ui"; +import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; +import { CYCLE_STATUS } from "constants/cycle"; +import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; +import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; +// components +// ui +// icons +// helpers // constants -import { CYCLE_STATUS } from "constants/cycle"; -import { EUserWorkspaceRoles } from "constants/workspace"; // types import { TCycleGroups } from "@plane/types"; -import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; type TCyclesListItem = { cycleId: string; @@ -45,8 +54,6 @@ export const CyclesListItem: FC = observer((props) => { } = useUser(); const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle(); const { getUserDetails } = useMember(); - // toast alert - const { setToastAlert } = useToast(); const handleCopyText = (e: MouseEvent) => { e.preventDefault(); @@ -54,8 +61,8 @@ export const CyclesListItem: FC = observer((props) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Cycle link copied to clipboard.", }); @@ -66,42 +73,56 @@ export const CyclesListItem: FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then( + () => { captureEvent(CYCLE_FAVORITED, { cycle_id: cycleId, element: "List layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) - .then(() => { - captureEvent(CYCLE_UNFAVORITED, { - cycle_id: cycleId, - element: "List layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + cycleId + ).then(() => { + captureEvent(CYCLE_UNFAVORITED, { + cycle_id: cycleId, + element: "List layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, + }); }; const handleEditCycle = (e: MouseEvent) => { @@ -206,7 +227,7 @@ export const CyclesListItem: FC = observer((props) => {
-
diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 173a7f4b7..f6ad64f99 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,15 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -// hooks -import { useUser } from "hooks/store"; // components import { CyclePeekOverview, CyclesListItem } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui import { Loader } from "@plane/ui"; // constants -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; export interface ICyclesList { cycleIds: string[]; @@ -20,15 +17,6 @@ export interface ICyclesList { export const CyclesList: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId } = props; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); - - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> @@ -54,12 +42,7 @@ export const CyclesList: FC = observer((props) => {
) : ( - + )} ) : ( diff --git a/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx index a321be0b5..745ca1bd3 100644 --- a/web/components/cycles/cycles-view.tsx +++ b/web/components/cycles/cycles-view.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useCycle } from "hooks/store"; // components import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles"; // ui components import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; +import { useCycle } from "hooks/store"; // types import { TCycleLayout, TCycleView } from "@plane/types"; @@ -32,10 +32,10 @@ export const CyclesView: FC = observer((props) => { filter === "completed" ? currentProjectCompletedCycleIds : filter === "draft" - ? currentProjectDraftCycleIds - : filter === "upcoming" - ? currentProjectUpcomingCycleIds - : currentProjectCycleIds; + ? currentProjectDraftCycleIds + : filter === "upcoming" + ? currentProjectUpcomingCycleIds + : currentProjectCycleIds; if (loader || !cyclesList) return ( diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 5dc0306ab..fd7b1f356 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -1,17 +1,16 @@ import { Fragment, useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // hooks +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { CYCLE_DELETED } from "constants/event-tracker"; import { useEventTracker, useCycle } from "hooks/store"; -import useToast from "hooks/use-toast"; // components -import { Button } from "@plane/ui"; // types import { ICycle } from "@plane/types"; // constants -import { CYCLE_DELETED } from "constants/event-tracker"; interface ICycleDelete { cycle: ICycle; @@ -31,8 +30,6 @@ export const CycleDeleteModal: React.FC = observer((props) => { // store hooks const { captureCycleEvent } = useEventTracker(); const { deleteCycle } = useCycle(); - // toast alert - const { setToastAlert } = useToast(); const formSubmit = async () => { if (!cycle) return; @@ -41,8 +38,8 @@ export const CycleDeleteModal: React.FC = observer((props) => { try { await deleteCycle(workspaceSlug, projectId, cycle.id) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle deleted successfully.", }); @@ -62,8 +59,8 @@ export const CycleDeleteModal: React.FC = observer((props) => { handleClose(); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Warning!", message: "Something went wrong please try again later.", }); diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 799d80438..4e2f55ef9 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -1,9 +1,9 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; // components +import { Button, Input, TextArea } from "@plane/ui"; import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns"; // ui -import { Button, Input, TextArea } from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 5d82c94a8..e9fdd50de 100644 --- a/web/components/cycles/gantt-chart/blocks.tsx +++ b/web/components/cycles/gantt-chart/blocks.tsx @@ -1,11 +1,11 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks -import { useApplication, useCycle } from "hooks/store"; // ui import { Tooltip, ContrastIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; +import { useApplication, useCycle } from "hooks/store"; type Props = { cycleId: string; @@ -33,12 +33,12 @@ export const CycleGanttBlock: React.FC = observer((props) => { cycleStatus === "current" ? "#09a953" : cycleStatus === "upcoming" - ? "#f7ae59" - : cycleStatus === "completed" - ? "#3f76ff" - : cycleStatus === "draft" - ? "rgb(var(--color-text-200))" - : "", + ? "#f7ae59" + : cycleStatus === "completed" + ? "#3f76ff" + : cycleStatus === "draft" + ? "rgb(var(--color-text-200))" + : "", }} onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project_id}/cycles/${cycleDetails?.id}`)} > @@ -86,12 +86,12 @@ export const CycleGanttSidebarBlock: React.FC = observer((props) => { cycleStatus === "current" ? "#09a953" : cycleStatus === "upcoming" - ? "#f7ae59" - : cycleStatus === "completed" - ? "#3f76ff" - : cycleStatus === "draft" - ? "rgb(var(--color-text-200))" - : "" + ? "#f7ae59" + : cycleStatus === "completed" + ? "#3f76ff" + : cycleStatus === "draft" + ? "rgb(var(--color-text-200))" + : "" }`} />
{cycleDetails?.name}
diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 646333aad..521273c51 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -1,15 +1,15 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { CycleGanttBlock } from "components/cycles"; +import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; +import { EUserProjectRoles } from "constants/project"; import { useCycle, useUser } from "hooks/store"; // components -import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; -import { CycleGanttBlock } from "components/cycles"; // types import { ICycle } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; type Props = { workspaceSlug: string; diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index b22afb2b4..2d1640ec9 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -1,17 +1,18 @@ import React, { useEffect, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; // services +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { CycleForm } from "components/cycles"; +import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker"; +import { useEventTracker, useCycle, useProject } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; import { CycleService } from "services/cycle.service"; // hooks -import { useEventTracker, useCycle, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; -import useLocalStorage from "hooks/use-local-storage"; // components -import { CycleForm } from "components/cycles"; +// ui // types import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types"; // constants -import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker"; type CycleModalProps = { isOpen: boolean; @@ -32,8 +33,6 @@ export const CycleCreateUpdateModal: React.FC = (props) => { const { captureCycleEvent } = useEventTracker(); const { workspaceProjectIds } = useProject(); const { createCycle, updateCycleDetails } = useCycle(); - // toast alert - const { setToastAlert } = useToast(); const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); @@ -43,8 +42,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { const selectedProjectId = payload.project_id ?? projectId.toString(); await createCycle(workspaceSlug, selectedProjectId, payload) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle created successfully.", }); @@ -54,8 +53,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Error in creating cycle. Please try again.", }); @@ -77,8 +76,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { eventName: CYCLE_UPDATED, payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle updated successfully.", }); @@ -88,8 +87,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { eventName: CYCLE_UPDATED, payload: { ...payload, state: "FAILED" }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Error in updating cycle. Please try again.", }); @@ -138,8 +137,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { } handleClose(); } else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "You already have a cycle on the given dates, if you want to create a draft cycle, remove the dates.", }); diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 646736bd2..adf986123 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -1,33 +1,31 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; +import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; -import isEmpty from "lodash/isEmpty"; -// services -import { CycleService } from "services/cycle.service"; -// hooks -import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; +// icons +import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react"; +// ui +import { Avatar, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; // components import { SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; import { CycleDeleteModal } from "components/cycles/delete-modal"; -// ui -import { Avatar, CustomMenu, Loader, LayersIcon } from "@plane/ui"; -// icons -import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, CalendarClock } from "lucide-react"; +import { DateRangeDropdown } from "components/dropdowns"; +// constants +import { CYCLE_STATUS } from "constants/cycle"; +import { CYCLE_UPDATED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; // helpers -import { copyUrlToClipboard } from "helpers/string.helper"; import { findHowManyDaysLeft, renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; +// services +import { CycleService } from "services/cycle.service"; // types import { ICycle } from "@plane/types"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { CYCLE_UPDATED } from "constants/event-tracker"; -// fetch-keys -import { CYCLE_STATUS } from "constants/cycle"; -import { DateRangeDropdown } from "components/dropdowns"; type Props = { cycleId: string; @@ -60,8 +58,6 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { // derived values const cycleDetails = getCycleById(cycleId); const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined; - // toast alert - const { setToastAlert } = useToast(); // form info const { control, reset } = useForm({ defaultValues, @@ -98,15 +94,15 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const handleCopyText = () => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Cycle link copied to clipboard.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Some error occurred", }); }); @@ -147,14 +143,14 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { if (isDateValid) { submitChanges(payload, "date_range"); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Cycle updated successfully.", }); } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "You already have a cycle on the given dates, if you want to create a draft cycle, you can do that by removing both the dates.", @@ -220,8 +216,8 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { ? "0 Issue" : `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}` : cycleDetails.total_issues === 0 - ? "0 Issue" - : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; + ? "0 Issue" + : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; const daysLeft = findHowManyDaysLeft(cycleDetails.end_date); @@ -302,7 +298,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { Date range
-
+
= observer((props) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const { setToastAlert } = useToast(); - const transferIssue = async (payload: any) => { if (!workspaceSlug || !projectId || !cycleId) return; // TODO: import transferIssuesFromCycle from store await transferIssuesFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), payload) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Issues transferred successfully", message: "Issues have been transferred successfully", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issues cannot be transfer. Please try again.", }); diff --git a/web/components/cycles/transfer-issues.tsx b/web/components/cycles/transfer-issues.tsx index 517df4421..921e67e4e 100644 --- a/web/components/cycles/transfer-issues.tsx +++ b/web/components/cycles/transfer-issues.tsx @@ -1,18 +1,18 @@ import React from "react"; +import isEmpty from "lodash/isEmpty"; import { useRouter } from "next/router"; import useSWR from "swr"; -import isEmpty from "lodash/isEmpty"; // component +import { AlertCircle } from "lucide-react"; import { Button, TransferIcon } from "@plane/ui"; // icon -import { AlertCircle } from "lucide-react"; // services +import { CYCLE_DETAILS } from "constants/fetch-keys"; import { CycleService } from "services/cycle.service"; // fetch-key -import { CYCLE_DETAILS } from "constants/fetch-keys"; type Props = { handleClick: () => void; diff --git a/web/components/dashboard/home-dashboard-widgets.tsx b/web/components/dashboard/home-dashboard-widgets.tsx index 2e2f9ef88..ab96ef90f 100644 --- a/web/components/dashboard/home-dashboard-widgets.tsx +++ b/web/components/dashboard/home-dashboard-widgets.tsx @@ -1,7 +1,5 @@ import { observer } from "mobx-react-lite"; // hooks -import { useApplication, useDashboard } from "hooks/store"; -// components import { AssignedIssuesWidget, CreatedIssuesWidget, @@ -13,6 +11,8 @@ import { RecentProjectsWidget, WidgetProps, } from "components/dashboard"; +import { useApplication, useDashboard } from "hooks/store"; +// components // types import { TWidgetKeys } from "@plane/types"; diff --git a/web/components/dashboard/project-empty-state.tsx b/web/components/dashboard/project-empty-state.tsx index bb7f82f34..32236e233 100644 --- a/web/components/dashboard/project-empty-state.tsx +++ b/web/components/dashboard/project-empty-state.tsx @@ -1,13 +1,13 @@ -import Image from "next/image"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; // hooks +import { Button } from "@plane/ui"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useApplication, useEventTracker, useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // assets import ProjectEmptyStateImage from "public/empty-state/dashboard/project.svg"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; export const DashboardProjectEmptyState = observer(() => { // store hooks diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index 7c8fbd2a9..1e031cacd 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks import { useDashboard } from "hooks/store"; @@ -17,7 +17,7 @@ import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashbo // types import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; // constants -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "assigned_issues"; @@ -30,8 +30,9 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -43,7 +44,10 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter); + const filterDates = getCustomDates( + filters.duration ?? selectedDurationFilter, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, issue_type: filters.tab ?? selectedTab, @@ -53,7 +57,7 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { }; useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter); + const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, @@ -81,8 +85,17 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { Assigned to you { + onChange={(val, customDates) => { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } + if (val === selectedDurationFilter) return; let newTab = selectedTab; diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index e7832883b..d36260f21 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Tab } from "@headlessui/react"; // hooks import { useDashboard } from "hooks/store"; @@ -17,7 +17,7 @@ import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashbo // types import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; // constants -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "created_issues"; @@ -30,8 +30,9 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -43,7 +44,10 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter); + const filterDates = getCustomDates( + filters.duration ?? selectedDurationFilter, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, issue_type: filters.tab ?? selectedTab, @@ -52,7 +56,7 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { }; useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter); + const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, @@ -78,8 +82,17 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { Created by you { + onChange={(val, customDates) => { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } + if (val === selectedDurationFilter) return; let newTab = selectedTab; diff --git a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx index 4844ea406..feef7ceca 100644 --- a/web/components/dashboard/widgets/dropdowns/duration-filter.tsx +++ b/web/components/dashboard/widgets/dropdowns/duration-filter.tsx @@ -1,36 +1,56 @@ +import { useState } from "react"; import { ChevronDown } from "lucide-react"; -// ui +// components import { CustomMenu } from "@plane/ui"; -// types -import { TDurationFilterOptions } from "@plane/types"; +import { DateFilterModal } from "components/core"; +// ui +// helpers +import { getDurationFilterDropdownLabel } from "helpers/dashboard.helper"; // constants -import { DURATION_FILTER_OPTIONS } from "constants/dashboard"; +import { DURATION_FILTER_OPTIONS, EDurationFilters } from "constants/dashboard"; type Props = { - onChange: (value: TDurationFilterOptions) => void; - value: TDurationFilterOptions; + customDates?: string[]; + onChange: (value: EDurationFilters, customDates?: string[]) => void; + value: EDurationFilters; }; export const DurationFilterDropdown: React.FC = (props) => { - const { onChange, value } = props; + const { customDates, onChange, value } = props; + // states + const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); return ( - - {DURATION_FILTER_OPTIONS.find((option) => option.key === value)?.label} - -
- } - placement="bottom-end" - closeOnSelect - > + <> + setIsDateFilterModalOpen(false)} + onSelect={(val) => onChange(EDurationFilters.CUSTOM, val)} + title="Due date" + /> + + {getDurationFilterDropdownLabel(value, customDates ?? [])} + +
+ } + placement="bottom-end" + closeOnSelect + > {DURATION_FILTER_OPTIONS.map((option) => ( - onChange(option.key)}> + { + if (option.key === "custom") setIsDateFilterModalOpen(true); + else onChange(option.key); + }} + > {option.label} ))} - + + ); }; diff --git a/web/components/dashboard/widgets/empty-states/assigned-issues.tsx b/web/components/dashboard/widgets/empty-states/assigned-issues.tsx index f60d8efe6..0cfad7dc9 100644 --- a/web/components/dashboard/widgets/empty-states/assigned-issues.tsx +++ b/web/components/dashboard/widgets/empty-states/assigned-issues.tsx @@ -1,9 +1,9 @@ import Image from "next/image"; import { useTheme } from "next-themes"; // types +import { ASSIGNED_ISSUES_EMPTY_STATES } from "constants/dashboard"; import { TIssuesListTypes } from "@plane/types"; // constants -import { ASSIGNED_ISSUES_EMPTY_STATES } from "constants/dashboard"; type Props = { type: TIssuesListTypes; diff --git a/web/components/dashboard/widgets/empty-states/created-issues.tsx b/web/components/dashboard/widgets/empty-states/created-issues.tsx index fe93d4404..2c59342fc 100644 --- a/web/components/dashboard/widgets/empty-states/created-issues.tsx +++ b/web/components/dashboard/widgets/empty-states/created-issues.tsx @@ -1,9 +1,9 @@ import Image from "next/image"; import { useTheme } from "next-themes"; // types +import { CREATED_ISSUES_EMPTY_STATES } from "constants/dashboard"; import { TIssuesListTypes } from "@plane/types"; // constants -import { CREATED_ISSUES_EMPTY_STATES } from "constants/dashboard"; type Props = { type: TIssuesListTypes; diff --git a/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx b/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx index 716a3afc1..a5279f715 100644 --- a/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx +++ b/web/components/dashboard/widgets/issue-panels/issue-list-item.tsx @@ -1,11 +1,11 @@ -import { observer } from "mobx-react-lite"; import isToday from "date-fns/isToday"; +import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail, useMember, useProject } from "hooks/store"; // ui import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui"; // helpers import { findTotalDaysInRange, renderFormattedDate } from "helpers/date-time.helper"; +import { useIssueDetail, useMember, useProject } from "hooks/store"; // types import { TIssue, TWidgetIssue } from "@plane/types"; diff --git a/web/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/components/dashboard/widgets/issue-panels/issues-list.tsx index 16b2b95d9..c429f3599 100644 --- a/web/components/dashboard/widgets/issue-panels/issues-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/issues-list.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; // hooks -import { useIssueDetail } from "hooks/store"; // components +import { Loader, getButtonStyling } from "@plane/ui"; import { AssignedCompletedIssueListItem, AssignedIssuesEmptyState, @@ -14,10 +14,10 @@ import { IssueListItemProps, } from "components/dashboard/widgets"; // ui -import { Loader, getButtonStyling } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; import { getRedirectionFilters } from "helpers/dashboard.helper"; +import { useIssueDetail } from "hooks/store"; // types import { TAssignedIssuesWidgetResponse, TCreatedIssuesWidgetResponse, TIssue, TIssuesListTypes } from "@plane/types"; diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx index 306c2fdeb..257f73851 100644 --- a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -3,12 +3,12 @@ import { Tab } from "@headlessui/react"; // helpers import { cn } from "helpers/common.helper"; // types -import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; +import { TIssuesListTypes } from "@plane/types"; // constants -import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; type Props = { - durationFilter: TDurationFilterOptions; + durationFilter: EDurationFilters; selectedTab: TIssuesListTypes; }; @@ -48,7 +48,7 @@ export const TabsList: React.FC = observer((props) => { className={cn( "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500", { - "text-custom-text-100 bg-custom-background-100": selectedTab === tab.key, + "text-custom-text-100": selectedTab === tab.key, "hover:text-custom-text-300": selectedTab !== tab.key, } )} diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index 91e321b05..becf32285 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -1,82 +1,37 @@ -import { useEffect, useState } from "react"; -import Link from "next/link"; +import { useEffect } from "react"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; // hooks import { useDashboard } from "hooks/store"; // components -import { MarimekkoGraph } from "components/ui"; import { DurationFilterDropdown, IssuesByPriorityEmptyState, WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// ui -import { PriorityIcon } from "@plane/ui"; // helpers import { getCustomDates } from "helpers/dashboard.helper"; // types import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; // constants -import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; -import { ISSUE_PRIORITIES } from "constants/issue"; - -const TEXT_COLORS = { - urgent: "#F4A9AA", - high: "#AB4800", - medium: "#AB6400", - low: "#1F2D5C", - none: "#60646C", -}; - -const CustomBar = (props: any) => { - const { bar, workspaceSlug } = props; - // states - const [isMouseOver, setIsMouseOver] = useState(false); - - return ( - - setIsMouseOver(true)} - onMouseLeave={() => setIsMouseOver(false)} - > - - - {bar?.id} - - - - ); -}; +import { IssuesByPriorityGraph } from "components/graphs"; +import { EDurationFilters } from "constants/dashboard"; const WIDGET_KEY = "issues_by_priority"; export const IssuesByPriorityWidget: React.FC = observer((props) => { const { dashboardId, workspaceSlug } = props; + // router + const router = useRouter(); // store hooks const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -86,7 +41,10 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDuration); + const filterDates = getCustomDates( + filters.duration ?? selectedDuration, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -94,7 +52,7 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => }; useEffect(() => { - const filterDates = getCustomDates(selectedDuration); + const filterDates = getCustomDates(selectedDuration, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -105,34 +63,13 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => if (!widgetDetails || !widgetStats) return ; const totalCount = widgetStats.reduce((acc, item) => acc + item?.count, 0); - const chartData = widgetStats - .filter((i) => i.count !== 0) - .map((item) => ({ - priority: item?.priority, - percentage: (item?.count / totalCount) * 100, - urgent: item?.priority === "urgent" ? 1 : 0, - high: item?.priority === "high" ? 1 : 0, - medium: item?.priority === "medium" ? 1 : 0, - low: item?.priority === "low" ? 1 : 0, - none: item?.priority === "none" ? 1 : 0, - })); - - const CustomBarsLayer = (props: any) => { - const { bars } = props; - - return ( - - {bars - ?.filter((b: any) => b?.value === 1) // render only bars with value 1 - .map((bar: any) => ( - - ))} - - ); - }; + const chartData = widgetStats.map((item) => ({ + priority: item?.priority, + priority_count: item?.count, + })); return ( -
+
= observer((props) => Assigned by priority + onChange={(val, customDates) => handleUpdateFilters({ duration: val, + ...(val === "custom" ? { custom_dates: customDates } : {}), }) } />
{totalCount > 0 ? ( -
-
- +
+ ({ - id: p.key, - value: p.key, - }))} - axisBottom={null} - axisLeft={null} - height="119px" - margin={{ - top: 11, - right: 0, - bottom: 0, - left: 0, + onBarClick={(datum) => { + router.push( + `/${workspaceSlug}/workspace-views/assigned?priority=${`${datum.data.priority}`.toLowerCase()}` + ); }} - defs={PRIORITY_GRAPH_GRADIENTS} - fill={ISSUE_PRIORITIES.map((p) => ({ - match: { - id: p.key, - }, - id: `gradient${p.title}`, - }))} - tooltip={() => <>} - enableGridX={false} - enableGridY={false} - layers={[CustomBarsLayer]} /> -
- {chartData.map((item) => ( -

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

- ))} -
) : ( -
+
)} diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index a0eb6c70f..1f093986c 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -1,24 +1,24 @@ import { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; // hooks -import { useDashboard } from "hooks/store"; -// components -import { PieGraph } from "components/ui"; import { DurationFilterDropdown, IssuesByStateGroupEmptyState, WidgetLoader, WidgetProps, } from "components/dashboard/widgets"; -// helpers +import { PieGraph } from "components/ui"; +import { STATE_GROUPS } from "constants/state"; import { getCustomDates } from "helpers/dashboard.helper"; +import { useDashboard } from "hooks/store"; +// components +// helpers // types import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; // constants -import { STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; -import { STATE_GROUPS } from "constants/state"; +import { EDurationFilters, STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "constants/dashboard"; const WIDGET_KEY = "issues_by_state_groups"; @@ -34,7 +34,8 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? "none"; + const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -44,7 +45,10 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) filters, }); - const filterDates = getCustomDates(filters.duration ?? selectedDuration); + const filterDates = getCustomDates( + filters.duration ?? selectedDuration, + filters.custom_dates ?? selectedCustomDates + ); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -53,7 +57,7 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) // fetch widget stats useEffect(() => { - const filterDates = getCustomDates(selectedDuration); + const filterDates = getCustomDates(selectedDuration, selectedCustomDates); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), @@ -75,14 +79,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) startedCount > 0 ? "started" : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; setActiveStateGroup(stateGroup); setDefaultStateGroup(stateGroup); @@ -139,10 +143,12 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) Assigned by state + onChange={(val, customDates) => handleUpdateFilters({ duration: val, + ...(val === "custom" ? { custom_dates: customDates } : {}), }) } /> diff --git a/web/components/dashboard/widgets/loaders/loader.tsx b/web/components/dashboard/widgets/loaders/loader.tsx index 141bb5533..ae4038b38 100644 --- a/web/components/dashboard/widgets/loaders/loader.tsx +++ b/web/components/dashboard/widgets/loaders/loader.tsx @@ -1,13 +1,13 @@ // components +import { TWidgetKeys } from "@plane/types"; import { AssignedIssuesWidgetLoader } from "./assigned-issues"; import { IssuesByPriorityWidgetLoader } from "./issues-by-priority"; import { IssuesByStateGroupWidgetLoader } from "./issues-by-state-group"; import { OverviewStatsWidgetLoader } from "./overview-stats"; import { RecentActivityWidgetLoader } from "./recent-activity"; -import { RecentProjectsWidgetLoader } from "./recent-projects"; import { RecentCollaboratorsWidgetLoader } from "./recent-collaborators"; +import { RecentProjectsWidgetLoader } from "./recent-projects"; // types -import { TWidgetKeys } from "@plane/types"; type Props = { widgetKey: TWidgetKeys; diff --git a/web/components/dashboard/widgets/loaders/recent-collaborators.tsx b/web/components/dashboard/widgets/loaders/recent-collaborators.tsx index d838967af..dc2163128 100644 --- a/web/components/dashboard/widgets/loaders/recent-collaborators.tsx +++ b/web/components/dashboard/widgets/loaders/recent-collaborators.tsx @@ -2,17 +2,16 @@ import { Loader } from "@plane/ui"; export const RecentCollaboratorsWidgetLoader = () => ( - - -
- {Array.from({ length: 8 }).map((_, index) => ( -
+ <> + {Array.from({ length: 8 }).map((_, index) => ( + +
- ))} -
- + + ))} + ); diff --git a/web/components/dashboard/widgets/overview-stats.tsx b/web/components/dashboard/widgets/overview-stats.tsx index 31bdee587..bfea5bf40 100644 --- a/web/components/dashboard/widgets/overview-stats.tsx +++ b/web/components/dashboard/widgets/overview-stats.tsx @@ -2,14 +2,14 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; // hooks +import { WidgetLoader } from "components/dashboard/widgets"; +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { useDashboard } from "hooks/store"; // components -import { WidgetLoader } from "components/dashboard/widgets"; // helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { TOverviewStatsWidgetResponse } from "@plane/types"; -import { cn } from "helpers/common.helper"; export type WidgetProps = { dashboardId: string; @@ -37,7 +37,7 @@ export const OverviewStatsWidget: React.FC = observer((props) => { key: "overdue", title: "Issues overdue", count: widgetStats?.pending_issues_count, - link: `/${workspaceSlug}/workspace-views/assigned/?target_date=${today};before`, + link: `/${workspaceSlug}/workspace-views/assigned/?state_group=backlog,unstarted,started&target_date=${today};before`, }, { key: "created", @@ -74,6 +74,7 @@ export const OverviewStatsWidget: React.FC = observer((props) => { > {STATS_LIST.map((stat, index) => (
= observer((props) => { // derived values const { fetchWidgetStats, getWidgetStats } = useDashboard(); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const redirectionLink = `/${workspaceSlug}/profile/${currentUser?.id}/activity`; useEffect(() => { fetchWidgetStats(workspaceSlug, dashboardId, { @@ -34,12 +36,12 @@ export const RecentActivityWidget: React.FC = observer((props) => { if (!widgetStats) return ; return ( -
- +
+ Your issue activities {widgetStats.length > 0 ? ( -
+
{widgetStats.map((activity) => (
@@ -47,7 +49,7 @@ export const RecentActivityWidget: React.FC = observer((props) => { activity.new_value === "restore" ? ( ) : ( -
+
) @@ -69,7 +71,7 @@ export const RecentActivityWidget: React.FC = observer((props) => {

- {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail.display_name}{" "} + {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "} {activity.field ? ( @@ -83,9 +85,18 @@ export const RecentActivityWidget: React.FC = observer((props) => {

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

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

- - ); -}); - -export const RecentCollaboratorsWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( -
-
-

Most active members

-

- Top eight active members in your project by last activity -

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

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

+ + ); +}); + +type CollaboratorsListProps = { + cursor: string; + dashboardId: string; + perPage: number; + searchQuery?: string; + updateIsLoading?: (isLoading: boolean) => void; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; + workspaceSlug: string; +}; + +const WIDGET_KEY = "recent_collaborators"; + +export const CollaboratorsList: React.FC = (props) => { + const { + cursor, + dashboardId, + perPage, + searchQuery = "", + updateIsLoading, + updateResultsCount, + updateTotalPages, + workspaceSlug, + } = props; + // store hooks + const { fetchWidgetStats } = useDashboard(); + + const { data: widgetStats } = useSWR( + workspaceSlug && dashboardId && cursor + ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}_${cursor}_${searchQuery}` + : null, + workspaceSlug && dashboardId && cursor + ? () => + fetchWidgetStats(workspaceSlug, dashboardId, { + cursor, + per_page: perPage, + search: searchQuery, + widget_key: WIDGET_KEY, + }) + : null + ) as { + data: TRecentCollaboratorsWidgetResponse | undefined; + }; + + useEffect(() => { + updateIsLoading?.(true); + + if (!widgetStats) return; + + updateIsLoading?.(false); + updateTotalPages(widgetStats.total_pages); + updateResultsCount(widgetStats.results?.length); + }, [updateIsLoading, updateResultsCount, updateTotalPages, widgetStats]); + + if (!widgetStats || !widgetStats?.results) return ; + + return ( + <> + {widgetStats?.results?.map((user) => ( + + ))} + + ); +}; diff --git a/web/components/dashboard/widgets/recent-collaborators/default-list.tsx b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx new file mode 100644 index 000000000..a27534bbf --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +// components +import { Button } from "@plane/ui"; +import { CollaboratorsList } from "./collaborators-list"; +// ui + +type Props = { + dashboardId: string; + perPage: number; + workspaceSlug: string; +}; + +export const DefaultCollaboratorsList: React.FC = (props) => { + const { dashboardId, perPage, workspaceSlug } = props; + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const collaboratorsPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + collaboratorsPages.push( + + ); + + return ( + <> +
+ {collaboratorsPages} +
+ {pageCount < totalPages && resultsCount !== 0 && ( +
+ +
+ )} + + ); +}; diff --git a/web/components/dashboard/widgets/recent-collaborators/index.ts b/web/components/dashboard/widgets/recent-collaborators/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/components/dashboard/widgets/recent-collaborators/root.tsx b/web/components/dashboard/widgets/recent-collaborators/root.tsx new file mode 100644 index 000000000..d65b15db7 --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/root.tsx @@ -0,0 +1,47 @@ +import { useState } from "react"; +import { Search } from "lucide-react"; +// types +import { WidgetProps } from "components/dashboard/widgets"; +// components +import { DefaultCollaboratorsList } from "./default-list"; +import { SearchedCollaboratorsList } from "./search-list"; + +const PER_PAGE = 8; + +export const RecentCollaboratorsWidget: React.FC = (props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [searchQuery, setSearchQuery] = useState(""); + + return ( +
+
+
+

Most active members

+

+ Top eight active members in your project by last activity +

+
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ {searchQuery.trim() !== "" ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/web/components/dashboard/widgets/recent-collaborators/search-list.tsx b/web/components/dashboard/widgets/recent-collaborators/search-list.tsx new file mode 100644 index 000000000..32baa72ad --- /dev/null +++ b/web/components/dashboard/widgets/recent-collaborators/search-list.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; +import Image from "next/image"; +import { useTheme } from "next-themes"; +// components +// ui +import { Button } from "@plane/ui"; +// assets +import DarkImage from "public/empty-state/dashboard/dark/recent-collaborators-1.svg"; +import LightImage from "public/empty-state/dashboard/light/recent-collaborators-1.svg"; +import { CollaboratorsList } from "./collaborators-list"; + +type Props = { + dashboardId: string; + perPage: number; + searchQuery: string; + workspaceSlug: string; +}; + +export const SearchedCollaboratorsList: React.FC = (props) => { + const { dashboardId, perPage, searchQuery, workspaceSlug } = props; + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + const [isLoading, setIsLoading] = useState(true); + // next-themes + const { resolvedTheme } = useTheme(); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const collaboratorsPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + collaboratorsPages.push( + + ); + + const emptyStateImage = resolvedTheme === "dark" ? DarkImage : LightImage; + + return ( + <> +
+ {collaboratorsPages} +
+ {!isLoading && totalPages === 0 && ( +
+
+ Recent collaborators +
+

No matching member

+
+ )} + {pageCount < totalPages && resultsCount !== 0 && ( +
+ +
+ )} + + ); +}; diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx index 79be92333..22e561ac8 100644 --- a/web/components/dashboard/widgets/recent-projects.tsx +++ b/web/components/dashboard/widgets/recent-projects.tsx @@ -1,20 +1,20 @@ import { useEffect } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Plus } from "lucide-react"; // hooks +import { Avatar, AvatarGroup } from "@plane/ui"; +import { WidgetLoader, WidgetProps } from "components/dashboard/widgets"; +import { PROJECT_BACKGROUND_COLORS } from "constants/dashboard"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; // components -import { WidgetLoader, WidgetProps } from "components/dashboard/widgets"; // ui -import { Avatar, AvatarGroup } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; // types import { TRecentProjectsWidgetResponse } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { PROJECT_BACKGROUND_COLORS } from "constants/dashboard"; const WIDGET_KEY = "recent_projects"; @@ -38,17 +38,9 @@ const ProjectListItem: React.FC = observer((props) => {
- {projectDetails.emoji ? ( - - {renderEmoji(projectDetails.emoji)} - - ) : projectDetails.icon_prop ? ( -
{renderEmoji(projectDetails.icon_prop)}
- ) : ( - - {projectDetails.name.charAt(0)} - - )} +
+ +
diff --git a/web/components/dropdowns/buttons.tsx b/web/components/dropdowns/buttons.tsx index 93d8c187c..d5d08a115 100644 --- a/web/components/dropdowns/buttons.tsx +++ b/web/components/dropdowns/buttons.tsx @@ -1,10 +1,10 @@ // helpers +import { Tooltip } from "@plane/ui"; import { cn } from "helpers/common.helper"; // types +import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants"; import { TButtonVariants } from "./types"; // constants -import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants"; -import { Tooltip } from "@plane/ui"; export type DropdownButtonProps = { children: React.ReactNode; @@ -31,8 +31,8 @@ export const DropdownButton: React.FC = (props) => { const ButtonToRender: React.FC = BORDER_BUTTON_VARIANTS.includes(variant) ? BorderButton : BACKGROUND_BUTTON_VARIANTS.includes(variant) - ? BackgroundButton - : TransparentButton; + ? BackgroundButton + : TransparentButton; return ( { +export const CycleOptions: FC = observer((props) => { const { projectId, isOpen, referenceElement, placement } = props; //state hooks diff --git a/web/components/dropdowns/cycle/index.tsx b/web/components/dropdowns/cycle/index.tsx index 465eb3e2a..8c08cd67d 100644 --- a/web/components/dropdowns/cycle/index.tsx +++ b/web/components/dropdowns/cycle/index.tsx @@ -3,19 +3,19 @@ import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks +import { ContrastIcon } from "@plane/ui"; +import { cn } from "helpers/common.helper"; import { useCycle } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "../buttons"; // icons -import { ContrastIcon } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; import { TDropdownProps } from "../types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; import { CycleOptions } from "./cycle-options"; type Props = TDropdownProps & { @@ -67,6 +67,7 @@ export const CycleDropdown: React.FC = observer((props) => { const toggleDropdown = () => { setIsOpen((prevIsOpen) => !prevIsOpen); + if (isOpen) onClose && onClose(); }; const dropdownOnChange = (val: string | null) => { diff --git a/web/components/dropdowns/date-range.tsx b/web/components/dropdowns/date-range.tsx index d3ef691b9..421ab41e6 100644 --- a/web/components/dropdowns/date-range.tsx +++ b/web/components/dropdowns/date-range.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; import { Placement } from "@popperjs/core"; import { DateRange, DayPicker, Matcher } from "react-day-picker"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { ArrowRight, CalendarDays } from "lucide-react"; // hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; // components -import { DropdownButton } from "./buttons"; // ui import { Button } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { DropdownButton } from "./buttons"; // types import { TButtonVariants } from "./types"; diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx index 570ea45da..049bf2250 100644 --- a/web/components/dropdowns/date.tsx +++ b/web/components/dropdowns/date.tsx @@ -1,20 +1,20 @@ import React, { useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; import { DayPicker, Matcher } from "react-day-picker"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { CalendarDays, X } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { clearIconClassName?: string; diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index 663ca67ce..bc977d1ce 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -1,21 +1,21 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; -import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search, Triangle } from "lucide-react"; import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react-lite"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; +import { Check, ChevronDown, Search, Triangle } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; import { useApplication, useEstimate } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; diff --git a/web/components/dropdowns/member/avatar.tsx b/web/components/dropdowns/member/avatar.tsx index 067d609c5..0f841b9e1 100644 --- a/web/components/dropdowns/member/avatar.tsx +++ b/web/components/dropdowns/member/avatar.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // hooks +import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui"; import { useMember } from "hooks/store"; // ui -import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui"; type AvatarProps = { showTooltip: boolean; diff --git a/web/components/dropdowns/member/index.tsx b/web/components/dropdowns/member/index.tsx index 332f2227a..0e9e36e21 100644 --- a/web/components/dropdowns/member/index.tsx +++ b/web/components/dropdowns/member/index.tsx @@ -3,19 +3,19 @@ import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; import { useMember } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { ButtonAvatars } from "./avatar"; import { DropdownButton } from "../buttons"; +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; +import { ButtonAvatars } from "./avatar"; // helpers -import { cn } from "helpers/common.helper"; // types +import { MemberOptions } from "./member-options"; import { MemberDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; -import { MemberOptions } from "./member-options"; type Props = { projectId?: string; @@ -67,6 +67,7 @@ export const MemberDropdown: React.FC = observer((props) => { const toggleDropdown = () => { setIsOpen((prevIsOpen) => !prevIsOpen); + if (isOpen) onClose && onClose(); }; const dropdownOnChange = (val: string & string[]) => { diff --git a/web/components/dropdowns/member/member-options.tsx b/web/components/dropdowns/member/member-options.tsx index 46a0b9cba..d91c6e0b1 100644 --- a/web/components/dropdowns/member/member-options.tsx +++ b/web/components/dropdowns/member/member-options.tsx @@ -1,16 +1,16 @@ import { useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; //components +import { Check, Search } from "lucide-react"; import { Avatar } from "@plane/ui"; //store import { useApplication, useMember, useUser } from "hooks/store"; //hooks -import { usePopper } from "react-popper"; //icon -import { Check, Search } from "lucide-react"; //types -import { Placement } from "@popperjs/core"; interface Props { projectId?: string; diff --git a/web/components/dropdowns/module/index.tsx b/web/components/dropdowns/module/index.tsx index 5e0a3977f..882604712 100644 --- a/web/components/dropdowns/module/index.tsx +++ b/web/components/dropdowns/module/index.tsx @@ -3,19 +3,19 @@ import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; import { ChevronDown, X } from "lucide-react"; // hooks +import { DiceIcon, Tooltip } from "@plane/ui"; +import { cn } from "helpers/common.helper"; import { useModule } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "../buttons"; // icons -import { DiceIcon, Tooltip } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants"; import { TDropdownProps } from "../types"; // constants -import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants"; import { ModuleOptions } from "./module-options"; type Props = TDropdownProps & { @@ -71,7 +71,7 @@ const ButtonContent: React.FC = (props) => { {showCount ? (
{!hideIcon && } -
+
{value.length > 0 ? value.length === 1 ? `${getModuleById(value[0])?.name || "module"}` @@ -80,18 +80,18 @@ const ButtonContent: React.FC = (props) => {
) : value.length > 0 ? ( -
+
{value.map((moduleId) => { const moduleDetails = getModuleById(moduleId); return (
{!hideIcon && } {!hideText && ( - {moduleDetails?.name} + {moduleDetails?.name} )} {!disabled && ( @@ -266,8 +266,7 @@ export const ModuleDropdown: React.FC = observer((props) => { placeholder={placeholder} showCount={showCount} value={value} - // @ts-ignore - onChange={onChange} + onChange={onChange as any} /> diff --git a/web/components/dropdowns/module/module-options.tsx b/web/components/dropdowns/module/module-options.tsx index e7d205b12..8f6a66468 100644 --- a/web/components/dropdowns/module/module-options.tsx +++ b/web/components/dropdowns/module/module-options.tsx @@ -1,17 +1,17 @@ import { useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; import { observer } from "mobx-react"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; //components +import { Check, Search } from "lucide-react"; import { DiceIcon } from "@plane/ui"; //store +import { cn } from "helpers/common.helper"; import { useApplication, useModule } from "hooks/store"; //hooks -import { usePopper } from "react-popper"; -import { cn } from "helpers/common.helper"; //icon -import { Check, Search } from "lucide-react"; //types -import { Placement } from "@popperjs/core"; type DropdownOptions = | { diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index 5cacefb3f..2409971f3 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -1,21 +1,21 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search } from "lucide-react"; import { useTheme } from "next-themes"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; +import { Check, ChevronDown, Search } from "lucide-react"; // hooks +import { PriorityIcon, Tooltip } from "@plane/ui"; +import { ISSUE_PRIORITIES } from "constants/issue"; +import { cn } from "helpers/common.helper"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // icons -import { PriorityIcon, Tooltip } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types import { TIssuePriorities } from "@plane/types"; +import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { ISSUE_PRIORITIES } from "constants/issue"; -import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS, BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -58,7 +58,7 @@ const BorderButton = (props: ButtonProps) => { high: "bg-orange-500/20 text-orange-950 border-orange-500", medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500", low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100", - none: "bg-custom-background-80 border-custom-border-300", + none: "hover:bg-custom-background-80 border-custom-border-300", }; return ( @@ -197,7 +197,7 @@ const TransparentButton = (props: ButtonProps) => { high: "text-orange-950", medium: "text-yellow-950", low: "text-blue-950", - none: "", + none: "hover:text-custom-text-300", }; return ( @@ -342,8 +342,8 @@ export const PriorityDropdown: React.FC = (props) => { const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant) ? BorderButton : BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant) - ? BackgroundButton - : TransparentButton; + ? BackgroundButton + : TransparentButton; useEffect(() => { if (isOpen && inputRef.current) { diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index f6fb9205e..719b89802 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -1,21 +1,21 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks +import { cn } from "helpers/common.helper"; import { useProject } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; +import { ProjectLogo } from "components/project"; // helpers -import { cn } from "helpers/common.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; @@ -77,13 +77,11 @@ export const ProjectDropdown: React.FC = observer((props) => { query: `${projectDetails?.name}`, content: (
- - {projectDetails?.emoji - ? renderEmoji(projectDetails?.emoji) - : projectDetails?.icon_prop - ? renderEmoji(projectDetails?.icon_prop) - : null} - + {projectDetails && ( + + + + )} {projectDetails?.name}
), @@ -169,13 +167,9 @@ export const ProjectDropdown: React.FC = observer((props) => { showTooltip={showTooltip} variant={buttonVariant} > - {!hideIcon && ( - - {selectedProject?.emoji - ? renderEmoji(selectedProject?.emoji) - : selectedProject?.icon_prop - ? renderEmoji(selectedProject?.icon_prop) - : null} + {!hideIcon && selectedProject && ( + + )} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx index 9fa2f38c8..f34ef576c 100644 --- a/web/components/dropdowns/state.tsx +++ b/web/components/dropdowns/state.tsx @@ -1,22 +1,22 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search } from "lucide-react"; // hooks +import { StateGroupIcon } from "@plane/ui"; +import { cn } from "helpers/common.helper"; import { useApplication, useProjectState } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { DropdownButton } from "./buttons"; // icons -import { StateGroupIcon } from "@plane/ui"; // helpers -import { cn } from "helpers/common.helper"; // types +import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; // constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; type Props = TDropdownProps & { button?: ReactNode; diff --git a/web/components/emoji-icon-picker/emojis.json b/web/components/emoji-icon-picker/emojis.json deleted file mode 100644 index 73b9b800f..000000000 --- a/web/components/emoji-icon-picker/emojis.json +++ /dev/null @@ -1,1090 +0,0 @@ -[ - "8986", - "8987", - "9193", - "9194", - "9195", - "9196", - "9197", - "9198", - "9199", - "9200", - "9201", - "9202", - "9203", - "9208", - "9209", - "9210", - "9410", - "9748", - "9749", - "9757", - "9800", - "9801", - "9802", - "9803", - "9804", - "9805", - "9806", - "9807", - "9808", - "9809", - "9810", - "9811", - "9823", - "9855", - "9875", - "9889", - "9898", - "9899", - "9917", - "9918", - "9924", - "9925", - "9934", - "9935", - "9937", - "9939", - "9940", - "9961", - "9962", - "9968", - "9969", - "9970", - "9971", - "9972", - "9973", - "9975", - "9976", - "9977", - "9978", - "9981", - "9986", - "9989", - "9992", - "9993", - "9994", - "9995", - "9996", - "9997", - "9999", - "10002", - "10004", - "10006", - "10013", - "10017", - "10024", - "10035", - "10036", - "10052", - "10055", - "10060", - "10062", - "10067", - "10068", - "10069", - "10071", - "10083", - "10084", - "10133", - "10134", - "10135", - "10145", - "10160", - "10175", - "10548", - "10549", - "11013", - "11014", - "11015", - "11035", - "11036", - "11088", - "11093", - "12336", - "12349", - "12951", - "12953", - "126980", - "127183", - "127344", - "127345", - "127358", - "127359", - "127374", - "127377", - "127378", - "127379", - "127380", - "127381", - "127382", - "127383", - "127384", - "127385", - "127386", - "127489", - "127490", - "127514", - "127535", - "127538", - "127539", - "127540", - "127541", - "127542", - "127543", - "127544", - "127545", - "127546", - "127568", - "127569", - "127744", - "127745", - "127746", - "127747", - "127748", - "127749", - "127750", - "127751", - "127752", - "127753", - "127754", - "127755", - "127756", - "127757", - "127758", - "127759", - "127760", - "127761", - "127762", - "127763", - "127764", - "127765", - "127766", - "127767", - "127768", - "127769", - "127770", - "127771", - "127772", - "127773", - "127774", - "127775", - "127776", - "127777", - "127780", - "127781", - "127782", - "127783", - "127784", - "127785", - "127786", - "127787", - "127788", - "127789", - "127790", - "127791", - "127792", - "127793", - "127794", - "127795", - "127796", - "127797", - "127798", - "127799", - "127800", - "127801", - "127802", - "127803", - "127804", - "127805", - "127806", - "127807", - "127808", - "127809", - "127810", - "127811", - "127812", - "127813", - "127814", - "127815", - "127816", - "127817", - "127818", - "127819", - "127820", - "127821", - "127822", - "127823", - "127824", - "127825", - "127826", - "127827", - "127828", - "127829", - "127830", - "127831", - "127832", - "127833", - "127834", - "127835", - "127836", - "127837", - "127838", - "127839", - "127840", - "127841", - "127842", - "127843", - "127844", - "127845", - "127846", - "127847", - "127848", - "127849", - "127850", - "127851", - "127852", - "127853", - "127854", - "127855", - "127856", - "127857", - "127858", - "127859", - "127860", - "127861", - "127862", - "127863", - "127864", - "127865", - "127866", - "127867", - "127868", - "127869", - "127870", - "127871", - "127872", - "127873", - "127874", - "127875", - "127876", - "127877", - "127878", - "127879", - "127880", - "127881", - "127882", - "127883", - "127884", - "127885", - "127886", - "127887", - "127888", - "127889", - "127890", - "127891", - "127894", - "127895", - "127897", - "127898", - "127899", - "127902", - "127903", - "127904", - "127905", - "127906", - "127907", - "127908", - "127909", - "127910", - "127911", - "127912", - "127913", - "127914", - "127915", - "127916", - "127917", - "127918", - "127919", - "127920", - "127921", - "127922", - "127923", - "127924", - "127925", - "127926", - "127927", - "127928", - "127929", - "127930", - "127931", - "127932", - "127933", - "127934", - "127935", - "127936", - "127937", - "127938", - "127939", - "127940", - "127941", - "127942", - "127943", - "127944", - "127945", - "127946", - "127947", - "127948", - "127949", - "127950", - "127951", - "127952", - "127953", - "127954", - "127955", - "127956", - "127957", - "127958", - "127959", - "127960", - "127961", - "127962", - "127963", - "127964", - "127965", - "127966", - "127967", - "127968", - "127969", - "127970", - "127971", - "127972", - "127973", - "127974", - "127975", - "127976", - "127977", - "127978", - "127979", - "127980", - "127981", - "127982", - "127983", - "127984", - "127987", - "127988", - "127989", - "127991", - "127992", - "127993", - "127994", - "127995", - "127996", - "127997", - "127998", - "127999", - "128000", - "128001", - "128002", - "128003", - "128004", - "128005", - "128006", - "128007", - "128008", - "128009", - "128010", - "128011", - "128012", - "128013", - "128014", - "128015", - "128016", - "128017", - "128018", - "128019", - "128020", - "128021", - "128022", - "128023", - "128024", - "128025", - "128026", - "128027", - "128028", - "128029", - "128030", - "128031", - "128032", - "128033", - "128034", - "128035", - "128036", - "128037", - "128038", - "128039", - "128040", - "128041", - "128042", - "128043", - "128044", - "128045", - "128046", - "128047", - "128048", - "128049", - "128050", - "128051", - "128052", - "128053", - "128054", - "128055", - "128056", - "128057", - "128058", - "128059", - "128060", - "128061", - "128062", - "128063", - "128064", - "128065", - "128066", - "128067", - "128068", - "128069", - "128070", - "128071", - "128072", - "128073", - "128074", - "128075", - "128076", - "128077", - "128078", - "128079", - "128080", - "128081", - "128082", - "128083", - "128084", - "128085", - "128086", - "128087", - "128088", - "128089", - "128090", - "128091", - "128092", - "128093", - "128094", - "128095", - "128096", - "128097", - "128098", - "128099", - "128100", - "128101", - "128102", - "128103", - "128104", - "128105", - "128106", - "128107", - "128108", - "128109", - "128110", - "128111", - "128112", - "128113", - "128114", - "128115", - "128116", - "128117", - "128118", - "128119", - "128120", - "128121", - "128122", - "128123", - "128124", - "128125", - "128126", - "128127", - "128128", - "128129", - "128130", - "128131", - "128132", - "128133", - "128134", - "128135", - "128136", - "128137", - "128138", - "128139", - "128140", - "128141", - "128142", - "128143", - "128144", - "128145", - "128146", - "128147", - "128148", - "128149", - "128150", - "128151", - "128152", - "128153", - "128154", - "128155", - "128156", - "128157", - "128158", - "128159", - "128160", - "128161", - "128162", - "128163", - "128164", - "128165", - "128166", - "128167", - "128168", - "128169", - "128170", - "128171", - "128172", - "128173", - "128174", - "128175", - "128176", - "128177", - "128178", - "128179", - "128180", - "128181", - "128182", - "128183", - "128184", - "128185", - "128186", - "128187", - "128188", - "128189", - "128190", - "128191", - "128192", - "128193", - "128194", - "128195", - "128196", - "128197", - "128198", - "128199", - "128200", - "128201", - "128202", - "128203", - "128204", - "128205", - "128206", - "128207", - "128208", - "128209", - "128210", - "128211", - "128212", - "128213", - "128214", - "128215", - "128216", - "128217", - "128218", - "128219", - "128220", - "128221", - "128222", - "128223", - "128224", - "128225", - "128226", - "128227", - "128228", - "128229", - "128230", - "128231", - "128232", - "128233", - "128234", - "128235", - "128236", - "128237", - "128238", - "128239", - "128240", - "128241", - "128242", - "128243", - "128244", - "128245", - "128246", - "128247", - "128248", - "128249", - "128250", - "128251", - "128252", - "128253", - "128255", - "128256", - "128257", - "128258", - "128259", - "128260", - "128261", - "128262", - "128263", - "128264", - "128265", - "128266", - "128267", - "128268", - "128269", - "128270", - "128271", - "128272", - "128273", - "128274", - "128275", - "128276", - "128277", - "128278", - "128279", - "128280", - "128281", - "128282", - "128283", - "128284", - "128285", - "128286", - "128287", - "128288", - "128289", - "128290", - "128291", - "128292", - "128293", - "128294", - "128295", - "128296", - "128297", - "128298", - "128299", - "128300", - "128301", - "128302", - "128303", - "128304", - "128305", - "128306", - "128307", - "128308", - "128309", - "128310", - "128311", - "128312", - "128313", - "128314", - "128315", - "128316", - "128317", - "128329", - "128330", - "128331", - "128332", - "128333", - "128334", - "128336", - "128337", - "128338", - "128339", - "128340", - "128341", - "128342", - "128343", - "128344", - "128345", - "128346", - "128347", - "128348", - "128349", - "128350", - "128351", - "128352", - "128353", - "128354", - "128355", - "128356", - "128357", - "128358", - "128359", - "128367", - "128368", - "128371", - "128372", - "128373", - "128374", - "128375", - "128376", - "128377", - "128378", - "128391", - "128394", - "128395", - "128396", - "128397", - "128400", - "128405", - "128406", - "128420", - "128421", - "128424", - "128433", - "128434", - "128444", - "128450", - "128451", - "128452", - "128465", - "128466", - "128467", - "128476", - "128477", - "128478", - "128481", - "128483", - "128488", - "128495", - "128499", - "128506", - "128507", - "128508", - "128509", - "128510", - "128511", - "128512", - "128513", - "128514", - "128515", - "128516", - "128517", - "128518", - "128519", - "128520", - "128521", - "128522", - "128523", - "128524", - "128525", - "128526", - "128527", - "128528", - "128529", - "128530", - "128531", - "128532", - "128533", - "128534", - "128535", - "128536", - "128537", - "128538", - "128539", - "128540", - "128541", - "128542", - "128543", - "128544", - "128545", - "128546", - "128547", - "128548", - "128549", - "128550", - "128551", - "128552", - "128553", - "128554", - "128555", - "128556", - "128557", - "128558", - "128559", - "128560", - "128561", - "128562", - "128563", - "128564", - "128565", - "128566", - "128567", - "128568", - "128569", - "128570", - "128571", - "128572", - "128573", - "128574", - "128575", - "128576", - "128577", - "128578", - "128579", - "128580", - "128581", - "128582", - "128583", - "128584", - "128585", - "128586", - "128587", - "128588", - "128589", - "128590", - "128591", - "128640", - "128641", - "128642", - "128643", - "128644", - "128645", - "128646", - "128647", - "128648", - "128649", - "128650", - "128651", - "128652", - "128653", - "128654", - "128655", - "128656", - "128657", - "128658", - "128659", - "128660", - "128661", - "128662", - "128663", - "128664", - "128665", - "128666", - "128667", - "128668", - "128669", - "128670", - "128671", - "128672", - "128673", - "128674", - "128675", - "128676", - "128677", - "128678", - "128679", - "128680", - "128681", - "128682", - "128683", - "128684", - "128685", - "128686", - "128687", - "128688", - "128689", - "128690", - "128691", - "128692", - "128693", - "128694", - "128695", - "128696", - "128697", - "128698", - "128699", - "128700", - "128701", - "128702", - "128703", - "128704", - "128705", - "128706", - "128707", - "128708", - "128709", - "128715", - "128716", - "128717", - "128718", - "128719", - "128720", - "128721", - "128722", - "128736", - "128737", - "128738", - "128739", - "128740", - "128741", - "128745", - "128747", - "128748", - "128752", - "128755", - "128756", - "128757", - "128758", - "128759", - "128760", - "128761", - "128762", - "129296", - "129297", - "129298", - "129299", - "129300", - "129301", - "129302", - "129303", - "129304", - "129305", - "129306", - "129307", - "129308", - "129309", - "129310", - "129311", - "129312", - "129313", - "129314", - "129315", - "129316", - "129317", - "129318", - "129319", - "129320", - "129321", - "129322", - "129323", - "129324", - "129325", - "129326", - "129327", - "129328", - "129329", - "129330", - "129331", - "129332", - "129333", - "129334", - "129335", - "129336", - "129337", - "129338", - "129340", - "129341", - "129342", - "129344", - "129345", - "129346", - "129347", - "129348", - "129349", - "129351", - "129352", - "129353", - "129354", - "129355", - "129356", - "129357", - "129358", - "129359", - "129360", - "129361", - "129362", - "129363", - "129364", - "129365", - "129366", - "129367", - "129368", - "129369", - "129370", - "129371", - "129372", - "129373", - "129374", - "129375", - "129376", - "129377", - "129378", - "129379", - "129380", - "129381", - "129382", - "129383", - "129384", - "129385", - "129386", - "129387", - "129408", - "129409", - "129410", - "129411", - "129412", - "129413", - "129414", - "129415", - "129416", - "129417", - "129418", - "129419", - "129420", - "129421", - "129422", - "129423", - "129424", - "129425", - "129426", - "129427", - "129428", - "129429", - "129430", - "129431", - "129472", - "129488", - "129489", - "129490", - "129491", - "129492", - "129493", - "129494", - "129495", - "129496", - "129497", - "129498", - "129499", - "129500", - "129501", - "129502", - "129503", - "129504", - "129505", - "129506", - "129507", - "129508", - "129509", - "129510" -] diff --git a/web/components/emoji-icon-picker/helpers.ts b/web/components/emoji-icon-picker/helpers.ts deleted file mode 100644 index ab59a7b07..000000000 --- a/web/components/emoji-icon-picker/helpers.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const saveRecentEmoji = (emoji: string) => { - const recentEmojis = localStorage.getItem("recentEmojis"); - if (recentEmojis) { - const recentEmojisArray = recentEmojis.split(","); - if (recentEmojisArray.includes(emoji)) { - const index = recentEmojisArray.indexOf(emoji); - recentEmojisArray.splice(index, 1); - } - recentEmojisArray.unshift(emoji); - if (recentEmojisArray.length > 18) { - recentEmojisArray.pop(); - } - localStorage.setItem("recentEmojis", recentEmojisArray.join(",")); - } else { - localStorage.setItem("recentEmojis", emoji); - } -}; - -export const getRecentEmojis = () => { - const recentEmojis = localStorage.getItem("recentEmojis"); - if (recentEmojis) { - const recentEmojisArray = recentEmojis.split(","); - return recentEmojisArray; - } - return []; -}; diff --git a/web/components/emoji-icon-picker/icons.json b/web/components/emoji-icon-picker/icons.json deleted file mode 100644 index f844f22d4..000000000 --- a/web/components/emoji-icon-picker/icons.json +++ /dev/null @@ -1,607 +0,0 @@ -{ - "material_rounded": [ - { - "name": "search" - }, - { - "name": "home" - }, - { - "name": "menu" - }, - { - "name": "close" - }, - { - "name": "settings" - }, - { - "name": "done" - }, - { - "name": "check_circle" - }, - { - "name": "favorite" - }, - { - "name": "add" - }, - { - "name": "delete" - }, - { - "name": "arrow_back" - }, - { - "name": "star" - }, - { - "name": "logout" - }, - { - "name": "add_circle" - }, - { - "name": "cancel" - }, - { - "name": "arrow_drop_down" - }, - { - "name": "more_vert" - }, - { - "name": "check" - }, - { - "name": "check_box" - }, - { - "name": "toggle_on" - }, - { - "name": "open_in_new" - }, - { - "name": "refresh" - }, - { - "name": "login" - }, - { - "name": "radio_button_unchecked" - }, - { - "name": "more_horiz" - }, - { - "name": "apps" - }, - { - "name": "radio_button_checked" - }, - { - "name": "download" - }, - { - "name": "remove" - }, - { - "name": "toggle_off" - }, - { - "name": "bolt" - }, - { - "name": "arrow_upward" - }, - { - "name": "filter_list" - }, - { - "name": "delete_forever" - }, - { - "name": "autorenew" - }, - { - "name": "key" - }, - { - "name": "sort" - }, - { - "name": "sync" - }, - { - "name": "add_box" - }, - { - "name": "block" - }, - { - "name": "restart_alt" - }, - { - "name": "menu_open" - }, - { - "name": "shopping_cart_checkout" - }, - { - "name": "expand_circle_down" - }, - { - "name": "backspace" - }, - { - "name": "undo" - }, - { - "name": "done_all" - }, - { - "name": "do_not_disturb_on" - }, - { - "name": "open_in_full" - }, - { - "name": "double_arrow" - }, - { - "name": "sync_alt" - }, - { - "name": "zoom_in" - }, - { - "name": "done_outline" - }, - { - "name": "drag_indicator" - }, - { - "name": "fullscreen" - }, - { - "name": "star_half" - }, - { - "name": "settings_accessibility" - }, - { - "name": "reply" - }, - { - "name": "exit_to_app" - }, - { - "name": "unfold_more" - }, - { - "name": "library_add" - }, - { - "name": "cached" - }, - { - "name": "select_check_box" - }, - { - "name": "terminal" - }, - { - "name": "change_circle" - }, - { - "name": "disabled_by_default" - }, - { - "name": "swap_horiz" - }, - { - "name": "swap_vert" - }, - { - "name": "app_registration" - }, - { - "name": "download_for_offline" - }, - { - "name": "close_fullscreen" - }, - { - "name": "file_open" - }, - { - "name": "minimize" - }, - { - "name": "open_with" - }, - { - "name": "dataset" - }, - { - "name": "add_task" - }, - { - "name": "start" - }, - { - "name": "keyboard_voice" - }, - { - "name": "create_new_folder" - }, - { - "name": "forward" - }, - { - "name": "download" - }, - { - "name": "settings_applications" - }, - { - "name": "compare_arrows" - }, - { - "name": "redo" - }, - { - "name": "zoom_out" - }, - { - "name": "publish" - }, - { - "name": "html" - }, - { - "name": "token" - }, - { - "name": "switch_access_shortcut" - }, - { - "name": "fullscreen_exit" - }, - { - "name": "sort_by_alpha" - }, - { - "name": "delete_sweep" - }, - { - "name": "indeterminate_check_box" - }, - { - "name": "view_timeline" - }, - { - "name": "settings_backup_restore" - }, - { - "name": "arrow_drop_down_circle" - }, - { - "name": "assistant_navigation" - }, - { - "name": "sync_problem" - }, - { - "name": "clear_all" - }, - { - "name": "density_medium" - }, - { - "name": "heart_plus" - }, - { - "name": "filter_alt_off" - }, - { - "name": "expand" - }, - { - "name": "subdirectory_arrow_right" - }, - { - "name": "download_done" - }, - { - "name": "arrow_outward" - }, - { - "name": "123" - }, - { - "name": "swipe_left" - }, - { - "name": "auto_mode" - }, - { - "name": "saved_search" - }, - { - "name": "place_item" - }, - { - "name": "system_update_alt" - }, - { - "name": "javascript" - }, - { - "name": "search_off" - }, - { - "name": "output" - }, - { - "name": "select_all" - }, - { - "name": "fit_screen" - }, - { - "name": "swipe_up" - }, - { - "name": "dynamic_form" - }, - { - "name": "hide_source" - }, - { - "name": "swipe_right" - }, - { - "name": "switch_access_shortcut_add" - }, - { - "name": "browse_gallery" - }, - { - "name": "css" - }, - { - "name": "density_small" - }, - { - "name": "assistant_direction" - }, - { - "name": "check_small" - }, - { - "name": "youtube_searched_for" - }, - { - "name": "move_up" - }, - { - "name": "swap_horizontal_circle" - }, - { - "name": "data_thresholding" - }, - { - "name": "install_mobile" - }, - { - "name": "move_down" - }, - { - "name": "dataset_linked" - }, - { - "name": "keyboard_command_key" - }, - { - "name": "view_kanban" - }, - { - "name": "swipe_down" - }, - { - "name": "key_off" - }, - { - "name": "transcribe" - }, - { - "name": "send_time_extension" - }, - { - "name": "swipe_down_alt" - }, - { - "name": "swipe_left_alt" - }, - { - "name": "swipe_right_alt" - }, - { - "name": "swipe_up_alt" - }, - { - "name": "keyboard_option_key" - }, - { - "name": "cycle" - }, - { - "name": "rebase" - }, - { - "name": "rebase_edit" - }, - { - "name": "empty_dashboard" - }, - { - "name": "magic_exchange" - }, - { - "name": "acute" - }, - { - "name": "point_scan" - }, - { - "name": "step_into" - }, - { - "name": "cheer" - }, - { - "name": "emoticon" - }, - { - "name": "explosion" - }, - { - "name": "water_bottle" - }, - { - "name": "weather_hail" - }, - { - "name": "syringe" - }, - { - "name": "pill" - }, - { - "name": "genetics" - }, - { - "name": "allergy" - }, - { - "name": "medical_mask" - }, - { - "name": "body_fat" - }, - { - "name": "barefoot" - }, - { - "name": "infrared" - }, - { - "name": "wrist" - }, - { - "name": "metabolism" - }, - { - "name": "conditions" - }, - { - "name": "taunt" - }, - { - "name": "altitude" - }, - { - "name": "tibia" - }, - { - "name": "footprint" - }, - { - "name": "eyeglasses" - }, - { - "name": "man_3" - }, - { - "name": "woman_2" - }, - { - "name": "rheumatology" - }, - { - "name": "tornado" - }, - { - "name": "landslide" - }, - { - "name": "foggy" - }, - { - "name": "severe_cold" - }, - { - "name": "tsunami" - }, - { - "name": "vape_free" - }, - { - "name": "sign_language" - }, - { - "name": "emoji_symbols" - }, - { - "name": "clear_night" - }, - { - "name": "emoji_food_beverage" - }, - { - "name": "hive" - }, - { - "name": "thunderstorm" - }, - { - "name": "communication" - }, - { - "name": "rocket" - }, - { - "name": "pets" - }, - { - "name": "public" - }, - { - "name": "quiz" - }, - { - "name": "mood" - }, - { - "name": "gavel" - }, - { - "name": "eco" - }, - { - "name": "diamond" - }, - { - "name": "forest" - }, - { - "name": "rainy" - }, - { - "name": "skull" - } - ] -} diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx deleted file mode 100644 index 0c72b986a..000000000 --- a/web/components/emoji-icon-picker/index.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import React, { useEffect, useState, useRef } from "react"; -// headless ui -import { Tab, Transition, Popover } from "@headlessui/react"; -// react colors -import { TwitterPicker } from "react-color"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// types -import { Props } from "./types"; -// emojis -import emojis from "./emojis.json"; -import icons from "./icons.json"; -// helpers -import { getRecentEmojis, saveRecentEmoji } from "./helpers"; -import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; - -const tabOptions = [ - { - key: "emoji", - title: "Emoji", - }, - { - key: "icon", - title: "Icon", - }, -]; - -const EmojiIconPicker: React.FC = (props) => { - const { label, value, onChange, onIconColorChange, disabled = false } = props; - // states - const [isOpen, setIsOpen] = useState(false); - const [openColorPicker, setOpenColorPicker] = useState(false); - const [activeColor, setActiveColor] = useState("rgb(var(--color-text-200))"); - const [recentEmojis, setRecentEmojis] = useState([]); - - const buttonRef = useRef(null); - const emojiPickerRef = useRef(null); - - useEffect(() => { - setRecentEmojis(getRecentEmojis()); - }, []); - - useEffect(() => { - if (!value || value?.length === 0) onChange(getRandomEmoji()); - }, [value, onChange]); - - useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false)); - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), buttonRef, emojiPickerRef); - - return ( - - setIsOpen((prev) => !prev)} - className="outline-none" - disabled={disabled} - > - {label} - - - -
- - - {tabOptions.map((tab) => ( - - {({ selected }) => ( - - )} - - ))} - - - - {recentEmojis.length > 0 && ( -
-

Recent

-
- {recentEmojis.map((emoji) => ( - - ))} -
-
- )} -
-
-
- {emojis.map((emoji) => ( - - ))} -
-
-
-
- -
-
- {["#FF6B00", "#8CC1FF", "#FCBE1D", "#18904F", "#ADF672", "#05C3FF", "#000000"].map((curCol) => ( - setActiveColor(curCol)} - /> - ))} - -
-
- { - setActiveColor(color.hex); - if (onIconColorChange) onIconColorChange(color.hex); - }} - triangle="hide" - width="205px" - /> -
-
-
-
- {icons.material_rounded.map((icon, index) => ( - - ))} -
-
-
-
-
-
-
-
-
- ); -}; - -export default EmojiIconPicker; diff --git a/web/components/emoji-icon-picker/types.d.ts b/web/components/emoji-icon-picker/types.d.ts deleted file mode 100644 index 8a0b54342..000000000 --- a/web/components/emoji-icon-picker/types.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type Props = { - label: React.ReactNode; - value: any; - onChange: ( - data: - | string - | { - name: string; - color: string; - } - ) => void; - onIconColorChange?: (data: any) => void; - disabled?: boolean; - tabIndex?: number; -}; diff --git a/web/components/empty-state/comic-box-button.tsx b/web/components/empty-state/comic-box-button.tsx index 607d74a91..0bf546a2f 100644 --- a/web/components/empty-state/comic-box-button.tsx +++ b/web/components/empty-state/comic-box-button.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; +import { usePopper } from "react-popper"; import { Popover } from "@headlessui/react"; // popper -import { usePopper } from "react-popper"; // helper import { getButtonStyling } from "@plane/ui"; diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index 4a5aeca02..9ef216068 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -1,119 +1,150 @@ import React from "react"; +import Link from "next/link"; import Image from "next/image"; + +import { useTheme } from "next-themes"; +// hooks +import { useUser } from "hooks/store"; // components +import { Button, TButtonVariant } from "@plane/ui"; import { ComicBoxButton } from "./comic-box-button"; -// ui -import { Button, getButtonStyling } from "@plane/ui"; -// helper +// constant +import { EMPTY_STATE_DETAILS, EmptyStateKeys } from "constants/empty-state"; +// helpers import { cn } from "helpers/common.helper"; -type Props = { - title: string; - description?: string; - image: any; - primaryButton?: { - icon?: any; - text: string; - onClick: () => void; - }; - secondaryButton?: { - icon?: any; - text: string; - onClick: () => void; - }; - comicBox?: { - title: string; - description: string; - }; - size?: "sm" | "lg"; - disabled?: boolean; +export type EmptyStateProps = { + type: EmptyStateKeys; + size?: "sm" | "md" | "lg"; + layout?: "widget-simple" | "screen-detailed" | "screen-simple"; + additionalPath?: string; + primaryButtonOnClick?: () => void; + primaryButtonLink?: string; + secondaryButtonOnClick?: () => void; }; -export const EmptyState: React.FC = ({ - title, - description, - image, - primaryButton, - secondaryButton, - comicBox, - size = "sm", - disabled = false, -}) => { - const emptyStateHeader = ( +export const EmptyState: React.FC = (props) => { + const { + type, + size = "lg", + layout = "screen-detailed", + additionalPath = "", + primaryButtonOnClick, + primaryButtonLink, + secondaryButtonOnClick, + } = props; + // store + const { + membership: { currentWorkspaceRole, currentProjectRole }, + } = useUser(); + // theme + const { resolvedTheme } = useTheme(); + // current empty state details + const { key, title, description, path, primaryButton, secondaryButton, accessType, access } = + EMPTY_STATE_DETAILS[type]; + // resolved empty state path + const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${ + resolvedTheme === "light" ? "light" : "dark" + }.webp`; + // current access type + const currentAccessType = accessType === "workspace" ? currentWorkspaceRole : currentProjectRole; + // permission + const isEditingAllowed = currentAccessType && access && currentAccessType >= access; + const anyButton = primaryButton || secondaryButton; + + // primary button + const renderPrimaryButton = () => { + if (!primaryButton) return null; + + const commonProps = { + size: size, + variant: "primary" as TButtonVariant, + prependIcon: primaryButton.icon, + onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined, + disabled: !isEditingAllowed, + }; + + if (primaryButton.comicBox) { + return ( + + ); + } else if (primaryButtonLink) { + return ( + + + + ); + } else { + return ; + } + }; + // secondary button + const renderSecondaryButton = () => { + if (!secondaryButton) return null; + + return ( + + ); + }; + + return ( <> - {description ? ( - <> -

{title}

-

{description}

- - ) : ( -

{title}

+ {layout === "screen-detailed" && ( +
+
+
+ {description ? ( + <> +

{title}

+

{description}

+ + ) : ( +

{title}

+ )} +
+ + {path && ( + {key + )} + + {anyButton && ( + <> +
+ {renderPrimaryButton()} + {renderSecondaryButton()} +
+ + )} +
+
)} ); - - const secondaryButtonElement = secondaryButton && ( - - ); - - return ( -
-
-
{emptyStateHeader}
- - {primaryButton?.text - -
- {primaryButton && ( - <> -
- {comicBox ? ( - primaryButton.onClick()} - disabled={disabled} - /> - ) : ( -
primaryButton.onClick()} - > - {primaryButton.icon} - {primaryButton.text} -
- )} -
- - )} - - {secondaryButton && secondaryButtonElement} -
-
-
- ); }; diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 0a607e88d..bc2dfb77d 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -1,15 +1,14 @@ import React, { useEffect } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; // store hooks -import { useEstimate } from "hooks/store"; -import useToast from "hooks/use-toast"; -// ui -import { Button, Input, TextArea } from "@plane/ui"; -// helpers +import { Button, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; import { checkDuplicates } from "helpers/array.helper"; +import { useEstimate } from "hooks/store"; +// ui +// helpers // types import { IEstimate, IEstimateFormData } from "@plane/types"; @@ -40,8 +39,6 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { // store hooks const { createEstimate, updateEstimate } = useEstimate(); // form info - // toast alert - const { setToastAlert } = useToast(); const { formState: { errors, isSubmitting }, handleSubmit, @@ -67,8 +64,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? err.status === 400 @@ -89,8 +86,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate could not be updated. Please try again.", }); @@ -99,8 +96,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { const onSubmit = async (formData: FormValues) => { if (!formData.name || formData.name === "") { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate title cannot be empty.", }); @@ -115,8 +112,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { formData.value5 === "" || formData.value6 === "" ) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate point cannot be empty.", }); @@ -131,8 +128,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { formData.value5.length > 20 || formData.value6.length > 20 ) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate point cannot have more than 20 characters.", }); @@ -149,8 +146,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { formData.value6, ]) ) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Estimate points cannot have duplicate values.", }); @@ -272,7 +269,7 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { {Array(6) .fill(0) .map((_, i) => ( -
+
{i + 1} @@ -315,8 +312,8 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { ? "Updating Estimate..." : "Update Estimate" : isSubmitting - ? "Creating Estimate..." - : "Create Estimate"} + ? "Creating Estimate..." + : "Create Estimate"}
diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx index 8055ddb90..f8bc2a65b 100644 --- a/web/components/estimates/delete-estimate-modal.tsx +++ b/web/components/estimates/delete-estimate-modal.tsx @@ -1,15 +1,14 @@ import React, { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; // store hooks +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { useEstimate } from "hooks/store"; -import useToast from "hooks/use-toast"; // types import { IEstimate } from "@plane/types"; // ui -import { Button } from "@plane/ui"; type Props = { isOpen: boolean; @@ -26,12 +25,11 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { deleteEstimate } = useEstimate(); - // toast alert - const { setToastAlert } = useToast(); const handleEstimateDelete = () => { if (!workspaceSlug || !projectId) return; + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain const estimateId = data?.id!; deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) @@ -43,8 +41,8 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate could not be deleted. Please try again", }); diff --git a/web/components/estimates/estimate-list-item.tsx b/web/components/estimates/estimate-list-item.tsx index b6effa711..c63c4b208 100644 --- a/web/components/estimates/estimate-list-item.tsx +++ b/web/components/estimates/estimate-list-item.tsx @@ -1,15 +1,14 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; -// ui -import { Button, CustomMenu } from "@plane/ui"; -//icons import { Pencil, Trash2 } from "lucide-react"; -// helpers +import { Button, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { orderArrayBy } from "helpers/array.helper"; +import { useProject } from "hooks/store"; +// ui +//icons +// helpers // types import { IEstimate } from "@plane/types"; @@ -26,8 +25,6 @@ export const EstimateListItem: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { currentProjectDetails, updateProject } = useProject(); - // hooks - const { setToastAlert } = useToast(); const handleUseEstimate = async () => { if (!workspaceSlug || !projectId) return; @@ -38,8 +35,8 @@ export const EstimateListItem: React.FC = observer((props) => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate points could not be used. Please try again.", }); diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 1dabc6181..1769ba016 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -1,21 +1,19 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; +import { useRouter } from "next/router"; // store hooks -import { useEstimate, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +import { useEstimate, useProject } from "hooks/store"; // components import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui -import { Button, Loader } from "@plane/ui"; +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IEstimate } from "@plane/types"; // helpers import { orderArrayBy } from "helpers/array.helper"; // constants -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; export const EstimatesList: React.FC = observer(() => { // states @@ -25,14 +23,9 @@ export const EstimatesList: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { updateProject, currentProjectDetails } = useProject(); const { projectEstimates, getProjectEstimateById } = useEstimate(); - const { currentUser } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const editEstimate = (estimate: IEstimate) => { setEstimateFormOpen(true); @@ -50,18 +43,14 @@ export const EstimatesList: React.FC = observer(() => { const error = err?.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "Estimate could not be disabled. Please try again", }); }); }; - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["estimate"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "estimates", isLightMode); - return ( <> { ) : (
- +
) ) : ( diff --git a/web/components/exporter/export-modal.tsx b/web/components/exporter/export-modal.tsx index b1f529775..16f8d4640 100644 --- a/web/components/exporter/export-modal.tsx +++ b/web/components/exporter/export-modal.tsx @@ -1,15 +1,14 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; // hooks +import { Button, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; + import { useProject } from "hooks/store"; // services import { ProjectExportService } from "services/project"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSearchSelect } from "@plane/ui"; // types import { IUser, IImporterService } from "@plane/types"; @@ -34,8 +33,6 @@ export const Exporter: React.FC = observer((props) => { const { workspaceSlug } = router.query; // store hooks const { workspaceProjectIds, getProjectById } = useProject(); - // toast alert - const { setToastAlert } = useToast(); const options = workspaceProjectIds?.map((projectId) => { const projectDetails = getProjectById(projectId); @@ -71,8 +68,8 @@ export const Exporter: React.FC = observer((props) => { mutateServices(); router.push(`/${workspaceSlug}/settings/exports`); setExportLoading(false); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Export Successful", message: `You will be able to download the exported ${ provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : "" @@ -81,8 +78,8 @@ export const Exporter: React.FC = observer((props) => { }) .catch(() => { setExportLoading(false); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Export was unsuccessful. Please try again.", }); diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index ed6a39220..03d925b62 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -1,11 +1,10 @@ import { useState } from "react"; -import Link from "next/link"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; -import useSWR, { mutate } from "swr"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import useSWR, { mutate } from "swr"; // hooks import { useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; @@ -13,18 +12,16 @@ import useUserAuth from "hooks/use-user-auth"; import { IntegrationService } from "services/integrations"; // components import { Exporter, SingleExport } from "components/exporter"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { ImportExportSettingsLoader } from "components/ui"; +import { EmptyState } from "components/empty-state"; // ui import { Button } from "@plane/ui"; -import { ImportExportSettingsLoader } from "components/ui"; // icons import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; -// fetch-keys -import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; // constants +import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; import { EXPORTERS_LIST } from "constants/workspace"; - -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -37,8 +34,6 @@ const IntegrationGuide = observer(() => { // router const router = useRouter(); const { workspaceSlug, provider } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { currentUser, currentUserLoader } = useUser(); // custom hooks @@ -51,10 +46,6 @@ const IntegrationGuide = observer(() => { : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["export"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "exports", isLightMode); - const handleCsvClose = () => { router.replace(`/${workspaceSlug?.toString()}/settings/exports`); }; @@ -150,12 +141,7 @@ const IntegrationGuide = observer(() => {
) : (
- +
) ) : ( diff --git a/web/components/exporter/single-export.tsx b/web/components/exporter/single-export.tsx index 34e41fc35..4fdcb4a15 100644 --- a/web/components/exporter/single-export.tsx +++ b/web/components/exporter/single-export.tsx @@ -38,12 +38,12 @@ export const SingleExport: FC = ({ service, refreshing }) => { service.status === "completed" ? "bg-green-500/20 text-green-500" : service.status === "processing" - ? "bg-yellow-500/20 text-yellow-500" - : service.status === "failed" - ? "bg-red-500/20 text-red-500" - : service.status === "expired" - ? "bg-orange-500/20 text-orange-500" - : "" + ? "bg-yellow-500/20 text-yellow-500" + : service.status === "failed" + ? "bg-red-500/20 text-red-500" + : service.status === "expired" + ? "bg-orange-500/20 text-orange-500" + : "" }`} > {refreshing ? "Refreshing..." : service.status} diff --git a/web/components/gantt-chart/blocks/block.tsx b/web/components/gantt-chart/blocks/block.tsx index 1e0882aee..3305c9846 100644 --- a/web/components/gantt-chart/blocks/block.tsx +++ b/web/components/gantt-chart/blocks/block.tsx @@ -1,16 +1,16 @@ import { observer } from "mobx-react"; // hooks -import { useGanttChart } from "../hooks"; -import { useIssueDetail } from "hooks/store"; // components -import { ChartAddBlock, ChartDraggable } from "../helpers"; // helpers import { cn } from "helpers/common.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { useIssueDetail } from "hooks/store"; // types -import { IBlockUpdateData, IGanttBlock } from "../types"; // constants import { BLOCK_HEIGHT } from "../constants"; +import { ChartAddBlock, ChartDraggable } from "../helpers"; +import { useGanttChart } from "../hooks"; +import { IBlockUpdateData, IGanttBlock } from "../types"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/blocks/blocks-list.tsx b/web/components/gantt-chart/blocks/blocks-list.tsx index d98524ecc..8eb1d8772 100644 --- a/web/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/components/gantt-chart/blocks/blocks-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; // components +import { HEADER_HEIGHT } from "../constants"; +import { IBlockUpdateData, IGanttBlock } from "../types"; import { GanttChartBlock } from "./block"; // types -import { IBlockUpdateData, IGanttBlock } from "../types"; // constants -import { HEADER_HEIGHT } from "../constants"; export type GanttChartBlocksProps = { itemsContainerWidth: number; @@ -47,6 +47,7 @@ export const GanttChartBlocksList: FC = (props) => { return ( = observer(() => { // chart hook diff --git a/web/components/gantt-chart/contexts/index.tsx b/web/components/gantt-chart/contexts/index.tsx index 1d8a19f1a..752645f66 100644 --- a/web/components/gantt-chart/contexts/index.tsx +++ b/web/components/gantt-chart/contexts/index.tsx @@ -1,4 +1,4 @@ -import { createContext } from "react"; +import React, { FC, createContext } from "react"; // mobx store import { GanttStore } from "store/issue/issue_gantt_view.store"; @@ -7,13 +7,17 @@ let ganttViewStore = new GanttStore(); export const GanttStoreContext = createContext(ganttViewStore); const initializeStore = () => { - const _ganttStore = ganttViewStore ?? new GanttStore(); - if (typeof window === "undefined") return _ganttStore; - if (!ganttViewStore) ganttViewStore = _ganttStore; - return _ganttStore; + const newGanttViewStore = ganttViewStore ?? new GanttStore(); + if (typeof window === "undefined") return newGanttViewStore; + if (!ganttViewStore) ganttViewStore = newGanttViewStore; + return newGanttViewStore; }; -export const GanttStoreProvider = ({ children }: any) => { +type GanttStoreProviderProps = { + children: React.ReactNode; +}; + +export const GanttStoreProvider: FC = ({ children }) => { const store = initializeStore(); return {children}; }; diff --git a/web/components/gantt-chart/helpers/add-block.tsx b/web/components/gantt-chart/helpers/add-block.tsx index b7497013f..d12c9f20e 100644 --- a/web/components/gantt-chart/helpers/add-block.tsx +++ b/web/components/gantt-chart/helpers/add-block.tsx @@ -1,14 +1,14 @@ import { useEffect, useRef, useState } from "react"; import { addDays } from "date-fns"; +import { observer } from "mobx-react"; import { Plus } from "lucide-react"; // ui import { Tooltip } from "@plane/ui"; // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { IBlockUpdateData, IGanttBlock } from "../types"; import { useGanttChart } from "../hooks/use-gantt-chart"; -import { observer } from "mobx-react"; +import { IBlockUpdateData, IGanttBlock } from "../types"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/helpers/draggable.tsx b/web/components/gantt-chart/helpers/draggable.tsx index c2b4dc619..54590c372 100644 --- a/web/components/gantt-chart/helpers/draggable.tsx +++ b/web/components/gantt-chart/helpers/draggable.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; import { ArrowRight } from "lucide-react"; // hooks import { IGanttBlock } from "components/gantt-chart"; @@ -7,7 +8,6 @@ import { cn } from "helpers/common.helper"; // constants import { SIDEBAR_WIDTH } from "../constants"; import { useGanttChart } from "../hooks/use-gantt-chart"; -import { observer } from "mobx-react"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/cycles/block.tsx b/web/components/gantt-chart/sidebar/cycles/block.tsx index f1374c753..6e780c479 100644 --- a/web/components/gantt-chart/sidebar/cycles/block.tsx +++ b/web/components/gantt-chart/sidebar/cycles/block.tsx @@ -2,16 +2,16 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react"; import { MoreVertical } from "lucide-react"; // hooks +import { CycleGanttSidebarBlock } from "components/cycles"; +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; import { useGanttChart } from "components/gantt-chart/hooks"; // components -import { CycleGanttSidebarBlock } from "components/cycles"; // helpers +import { IGanttBlock } from "components/gantt-chart/types"; import { cn } from "helpers/common.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper"; // types -import { IGanttBlock } from "components/gantt-chart/types"; // constants -import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/cycles/sidebar.tsx b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx index 11f67a099..e47b2304e 100644 --- a/web/components/gantt-chart/sidebar/cycles/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx @@ -2,9 +2,9 @@ import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; import { CyclesSidebarBlock } from "./block"; // types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; type Props = { title: string; diff --git a/web/components/gantt-chart/sidebar/issues/block.tsx b/web/components/gantt-chart/sidebar/issues/block.tsx index 03a17a65b..92fc32664 100644 --- a/web/components/gantt-chart/sidebar/issues/block.tsx +++ b/web/components/gantt-chart/sidebar/issues/block.tsx @@ -2,17 +2,17 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react"; import { MoreVertical } from "lucide-react"; // hooks -import { useIssueDetail } from "hooks/store"; import { useGanttChart } from "components/gantt-chart/hooks"; // components import { IssueGanttSidebarBlock } from "components/issues"; // helpers import { cn } from "helpers/common.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper"; +import { useIssueDetail } from "hooks/store"; // types -import { IGanttBlock } from "../../types"; // constants import { BLOCK_HEIGHT } from "../../constants"; +import { IGanttBlock } from "../../types"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/components/gantt-chart/sidebar/issues/sidebar.tsx index 323938eec..e82e40f5d 100644 --- a/web/components/gantt-chart/sidebar/issues/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -1,10 +1,10 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; // components -import { IssuesSidebarBlock } from "./block"; // ui import { Loader } from "@plane/ui"; // types import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; +import { IssuesSidebarBlock } from "./block"; type Props = { blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; diff --git a/web/components/gantt-chart/sidebar/modules/block.tsx b/web/components/gantt-chart/sidebar/modules/block.tsx index 4b2e47226..41647644f 100644 --- a/web/components/gantt-chart/sidebar/modules/block.tsx +++ b/web/components/gantt-chart/sidebar/modules/block.tsx @@ -2,16 +2,16 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react"; import { MoreVertical } from "lucide-react"; // hooks +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; import { useGanttChart } from "components/gantt-chart/hooks"; // components +import { IGanttBlock } from "components/gantt-chart/types"; import { ModuleGanttSidebarBlock } from "components/modules"; // helpers import { cn } from "helpers/common.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper"; // types -import { IGanttBlock } from "components/gantt-chart/types"; // constants -import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; type Props = { block: IGanttBlock; diff --git a/web/components/gantt-chart/sidebar/modules/sidebar.tsx b/web/components/gantt-chart/sidebar/modules/sidebar.tsx index dee83fa79..a4bcbd5ec 100644 --- a/web/components/gantt-chart/sidebar/modules/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/modules/sidebar.tsx @@ -2,9 +2,9 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; import { ModulesSidebarBlock } from "./block"; // types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; type Props = { title: string; diff --git a/web/components/gantt-chart/sidebar/project-views.tsx b/web/components/gantt-chart/sidebar/project-views.tsx index a7e7c5e35..92a677b19 100644 --- a/web/components/gantt-chart/sidebar/project-views.tsx +++ b/web/components/gantt-chart/sidebar/project-views.tsx @@ -2,9 +2,9 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea // ui import { Loader } from "@plane/ui"; // components +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; import { IssuesSidebarBlock } from "./issues/block"; // types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; type Props = { title: string; diff --git a/web/components/gantt-chart/views/bi-week-view.ts b/web/components/gantt-chart/views/bi-week-view.ts index 14c0aad15..6ace4bcc4 100644 --- a/web/components/gantt-chart/views/bi-week-view.ts +++ b/web/components/gantt-chart/views/bi-week-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; diff --git a/web/components/gantt-chart/views/day-view.ts b/web/components/gantt-chart/views/day-view.ts index 0801b7bb1..e8da6801c 100644 --- a/web/components/gantt-chart/views/day-view.ts +++ b/web/components/gantt-chart/views/day-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; export const getWeekNumberByDate = (date: Date) => { const firstDayOfYear = new Date(date.getFullYear(), 0, 1); diff --git a/web/components/gantt-chart/views/helpers.ts b/web/components/gantt-chart/views/helpers.ts index 94b614286..4bd295ce3 100644 --- a/web/components/gantt-chart/views/helpers.ts +++ b/web/components/gantt-chart/views/helpers.ts @@ -56,8 +56,8 @@ export const getAllDatesInWeekByWeekNumber = (weekNumber: number, year: number) const startDate = new Date(firstDayOfYear.getTime()); startDate.setDate(startDate.getDate() + 7 * (weekNumber - 1)); - var datesInWeek = []; - for (var i = 0; i < 7; i++) { + const datesInWeek = []; + for (let i = 0; i < 7; i++) { const currentDate = new Date(startDate.getTime()); currentDate.setDate(currentDate.getDate() + i); datesInWeek.push(currentDate); diff --git a/web/components/gantt-chart/views/hours-view.ts b/web/components/gantt-chart/views/hours-view.ts index 0801b7bb1..e8da6801c 100644 --- a/web/components/gantt-chart/views/hours-view.ts +++ b/web/components/gantt-chart/views/hours-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; export const getWeekNumberByDate = (date: Date) => { const firstDayOfYear = new Date(date.getFullYear(), 0, 1); diff --git a/web/components/gantt-chart/views/month-view.ts b/web/components/gantt-chart/views/month-view.ts index 13d054da1..1e7e6d878 100644 --- a/web/components/gantt-chart/views/month-view.ts +++ b/web/components/gantt-chart/views/month-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType, IGanttBlock } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; @@ -178,7 +178,7 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, const positionDaysDifference: number = Math.abs(Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24))); scrollPosition = positionDaysDifference * chartData.data.width; - var diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12; + let diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12; diffMonths -= startDate.getMonth(); diffMonths += itemStartDate.getMonth(); diff --git a/web/components/gantt-chart/views/quater-view.ts b/web/components/gantt-chart/views/quater-view.ts index ed25974a3..9d45a43a1 100644 --- a/web/components/gantt-chart/views/quater-view.ts +++ b/web/components/gantt-chart/views/quater-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers"; diff --git a/web/components/gantt-chart/views/week-view.ts b/web/components/gantt-chart/views/week-view.ts index a65eb70b9..bd4ae383d 100644 --- a/web/components/gantt-chart/views/week-view.ts +++ b/web/components/gantt-chart/views/week-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; diff --git a/web/components/gantt-chart/views/year-view.ts b/web/components/gantt-chart/views/year-view.ts index 82d397e97..69ff9dae8 100644 --- a/web/components/gantt-chart/views/year-view.ts +++ b/web/components/gantt-chart/views/year-view.ts @@ -1,7 +1,7 @@ // types +import { weeks, months } from "../data"; import { ChartDataType } from "../types"; // data -import { weeks, months } from "../data"; // helpers import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers"; diff --git a/web/components/graphs/index.ts b/web/components/graphs/index.ts new file mode 100644 index 000000000..305c3944e --- /dev/null +++ b/web/components/graphs/index.ts @@ -0,0 +1 @@ +export * from "./issues-by-priority"; diff --git a/web/components/graphs/issues-by-priority.tsx b/web/components/graphs/issues-by-priority.tsx new file mode 100644 index 000000000..9dfe56891 --- /dev/null +++ b/web/components/graphs/issues-by-priority.tsx @@ -0,0 +1,103 @@ +import { ComputedDatum } from "@nivo/bar"; +import { Theme } from "@nivo/core"; +// components +import { BarGraph } from "components/ui"; +// helpers +import { PRIORITY_GRAPH_GRADIENTS } from "constants/dashboard"; +import { ISSUE_PRIORITIES } from "constants/issue"; +import { capitalizeFirstLetter } from "helpers/string.helper"; +// types +import { TIssuePriorities } from "@plane/types"; +// constants + +type Props = { + borderRadius?: number; + data: { + priority: TIssuePriorities; + priority_count: number; + }[]; + height?: number; + onBarClick?: ( + datum: ComputedDatum & { + color: string; + } + ) => void; + padding?: number; + theme?: Theme; +}; + +const PRIORITY_TEXT_COLORS = { + urgent: "#CE2C31", + high: "#AB4800", + medium: "#AB6400", + low: "#1F2D5C", + none: "#60646C", +}; + +export const IssuesByPriorityGraph: React.FC = (props) => { + const { borderRadius = 8, data, height = 300, onBarClick, padding = 0.05, theme } = props; + + const chartData = data.map((priority) => ({ + priority: capitalizeFirstLetter(priority.priority), + value: priority.priority_count, + })); + + return ( + p.priority_count)} + axisBottom={{ + tickPadding: 8, + tickSize: 0, + }} + tooltip={(datum) => ( +
+ + {datum.data.priority}: + {datum.value} +
+ )} + colors={({ data }) => `url(#gradient${data.priority})`} + defs={PRIORITY_GRAPH_GRADIENTS} + fill={ISSUE_PRIORITIES.map((p) => ({ + match: { + id: p.key, + }, + id: `gradient${p.title}`, + }))} + onClick={(datum) => { + if (onBarClick) onBarClick(datum); + }} + theme={{ + axis: { + domain: { + line: { + stroke: "transparent", + }, + }, + ticks: { + text: { + fontSize: 13, + }, + }, + }, + grid: { + line: { + stroke: "transparent", + }, + }, + ...theme, + }} + /> + ); +}; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 0a36c133b..5ef1ebf2c 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -1,8 +1,19 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks +import { ArrowRight, Plus, PanelRight } from "lucide-react"; +import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; +import { ProjectAnalyticsModal } from "components/analytics"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { cn } from "helpers/common.helper"; +import { truncateText } from "helpers/string.helper"; import { useApplication, useEventTracker, @@ -16,24 +27,13 @@ import { } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { ProjectAnalyticsModal } from "components/analytics"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; // icons -import { ArrowRight, Plus, PanelRight } from "lucide-react"; // helpers -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { cn } from "helpers/common.helper"; -import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { // router @@ -163,13 +163,9 @@ export const CycleIssuesHeader: React.FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -209,9 +205,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {currentProjectCycleIds?.map((cycleId) => ( - - ))} + {currentProjectCycleIds?.map((cycleId) => )} } /> @@ -244,6 +238,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { handleDisplayFiltersUpdate={handleDisplayFilters} displayProperties={issueFilters?.displayProperties ?? {}} handleDisplayPropertiesUpdate={handleDisplayProperties} + ignoreGroupedFilters={["cycle"]} /> diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index 496fabecd..22637147f 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -1,24 +1,25 @@ import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { List, Plus } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; -import { EUserProjectRoles } from "constants/project"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; -import { TCycleLayout } from "@plane/types"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; +import { TCycleLayout } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const CyclesHeader: FC = observer(() => { // router const router = useRouter(); + const { workspaceSlug } = router.query; // store hooks const { commandPalette: { toggleCreateCycleModal }, @@ -32,9 +33,6 @@ export const CyclesHeader: FC = observer(() => { const canUserCreateCycle = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - const { workspaceSlug } = router.query as { - workspaceSlug: string; - }; const { setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); const handleCurrentLayout = useCallback( @@ -58,13 +56,9 @@ export const CyclesHeader: FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -73,7 +67,9 @@ export const CyclesHeader: FC = observer(() => { /> } />} + link={ + } /> + } />
@@ -110,6 +106,7 @@ export const CyclesHeader: FC = observer(() => { > {CYCLE_VIEW_LAYOUTS.map((layout) => ( { // handleLayoutChange(ISSUE_LAYOUTS[index].key); handleCurrentLayout(layout.key as TCycleLayout); diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index 3c40cbbff..effe60fe4 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -1,23 +1,23 @@ import { useCallback, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; // hooks -import { useLabel, useMember, useUser, useIssues } from "hooks/store"; -// components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues"; -import { CreateUpdateWorkspaceViewModal } from "components/workspace"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; -// ui -import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui"; -// icons import { List, PlusIcon, Sheet } from "lucide-react"; +import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues"; +// components +import { CreateUpdateWorkspaceViewModal } from "components/workspace"; +// ui +// icons // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserWorkspaceRoles } from "constants/workspace"; +import { useLabel, useMember, useUser, useIssues } from "hooks/store"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; const GLOBAL_VIEW_LAYOUTS = [ { key: "list", title: "List", link: "/workspace-views", icon: List }, diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index f722b506f..10717ecc3 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -1,8 +1,19 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks +import { ArrowRight, PanelRight, Plus } from "lucide-react"; +import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui"; +import { ProjectAnalyticsModal } from "components/analytics"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +import { ModuleMobileHeader } from "components/modules/module-mobile-header"; +import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { cn } from "helpers/common.helper"; +import { truncateText } from "helpers/string.helper"; import { useApplication, useEventTracker, @@ -14,26 +25,16 @@ import { useUser, useIssues, } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; import useLocalStorage from "hooks/use-local-storage"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { ProjectAnalyticsModal } from "components/analytics"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, CustomMenu, DiceIcon, LayersIcon } from "@plane/ui"; // icons -import { ArrowRight, PanelRight, Plus } from "lucide-react"; // helpers -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants -import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { cn } from "helpers/common.helper"; -import { ModuleMobileHeader } from "components/modules/module-mobile-header"; const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { // router @@ -64,15 +65,12 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const [analyticsModal, setAnalyticsModal] = useState(false); // router const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; + const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { - issuesFilter: { issueFilters, updateFilters }, + issuesFilter: { issueFilters }, } = useIssues(EIssuesStoreType.MODULE); + const { updateFilters } = useIssuesActions(EIssuesStoreType.MODULE); const { projectModuleIds, getModuleById } = useModule(); const { commandPalette: { toggleCreateIssueModal }, @@ -99,15 +97,15 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); + if (!projectId) return; + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [projectId, moduleId, updateFilters] ); const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; + if (!projectId) return; const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { @@ -119,25 +117,25 @@ export const ModuleIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); + updateFilters(projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues }); }, - [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] + [projectId, moduleId, issueFilters, updateFilters] ); const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); + if (!projectId) return; + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [projectId, moduleId, updateFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); + if (!projectId) return; + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [projectId, moduleId, updateFilters] ); // derived values @@ -166,13 +164,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -212,9 +206,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {projectModuleIds?.map((moduleId) => ( - - ))} + {projectModuleIds?.map((moduleId) => )} } /> @@ -248,6 +240,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { handleDisplayFiltersUpdate={handleDisplayFilters} displayProperties={issueFilters?.displayProperties ?? {}} handleDisplayPropertiesUpdate={handleDisplayProperties} + ignoreGroupedFilters={["module"]} />
diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index 9ad34678a..a1233ae52 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -1,19 +1,19 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +// icons import { GanttChartSquare, LayoutGrid, List, Plus } from "lucide-react"; -// hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; // ui import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui"; -// helper -import { renderEmoji } from "helpers/emoji.helper"; +// components +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // constants import { MODULE_VIEW_LAYOUTS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -// components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +// hooks +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; +import { ProjectLogo } from "components/project"; export const ModulesListHeader: React.FC = observer(() => { // router @@ -45,13 +45,9 @@ export const ModulesListHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -71,14 +67,16 @@ export const ModulesListHeader: React.FC = observer(() => { @@ -106,7 +104,13 @@ export const ModulesListHeader: React.FC = observer(() => { // placement="bottom-start" customButton={ - {modulesView === 'gantt_chart' ? : modulesView === 'grid' ? : } + {modulesView === "gantt_chart" ? ( + + ) : modulesView === "grid" ? ( + + ) : ( + + )} Layout } @@ -115,6 +119,7 @@ export const ModulesListHeader: React.FC = observer(() => { > {MODULE_VIEW_LAYOUTS.map((layout) => ( setModulesView(layout.key)} className="flex items-center gap-2" > @@ -127,5 +132,3 @@ export const ModulesListHeader: React.FC = observer(() => {
); }); - - diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index e2a427db7..2c05d95fa 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -1,16 +1,16 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { FileText, Plus } from "lucide-react"; // hooks -import { useApplication, usePage, useProject } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; -// components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +// components +import { useApplication, usePage, useProject } from "hooks/store"; +import { ProjectLogo } from "components/project"; export interface IPagesHeaderProps { showButton?: boolean; @@ -42,13 +42,9 @@ export const PageDetailsHeader: FC = observer((props) => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index 1984971d6..e45d1a9fe 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -1,17 +1,17 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { FileText, Plus } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; -// constants -import { EUserProjectRoles } from "constants/project"; -// components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { EUserProjectRoles } from "constants/project"; +// constants +// components +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import { ProjectLogo } from "components/project"; export const PagesHeader = observer(() => { // router @@ -43,13 +43,9 @@ export const PagesHeader = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/profile-settings.tsx b/web/components/headers/profile-settings.tsx index 24c69f093..5c419f05b 100644 --- a/web/components/headers/profile-settings.tsx +++ b/web/components/headers/profile-settings.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; // ui -import { Breadcrumbs } from "@plane/ui"; import { Settings } from "lucide-react"; +import { Breadcrumbs } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; interface IProfileSettingHeader { diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx index 9d4596f83..7cf5c5673 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/components/headers/project-archived-issue-details.tsx @@ -1,22 +1,22 @@ import { FC } from "react"; -import useSWR from "swr"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // hooks -import { useProject } from "hooks/store"; -// ui import { Breadcrumbs, LayersIcon } from "@plane/ui"; -// types -import { TIssue } from "@plane/types"; -// constants -import { ISSUE_DETAILS } from "constants/fetch-keys"; -// services -import { IssueArchiveService } from "services/issue"; -// helpers -import { renderEmoji } from "helpers/emoji.helper"; -// components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; +import { useProject } from "hooks/store"; +// components +import { ProjectLogo } from "components/project"; +// ui +// types +import { IssueArchiveService } from "services/issue"; +// constants +// services +// helpers +// components const issueArchiveService = new IssueArchiveService(); @@ -25,9 +25,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, archivedIssueId } = router.query; // store hooks - const { currentProjectDetails, getProjectById } = useProject(); + const { currentProjectDetails } = useProject(); - const { data: issueDetails } = useSWR( + const { data: issueDetails } = useSWR( workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId as string) : null, workspaceSlug && projectId && archivedIssueId ? () => @@ -52,13 +52,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -82,8 +78,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { link={ } diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index d1da1c859..db208aa21 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -1,21 +1,21 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { ArrowLeft } from "lucide-react"; // hooks -import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // ui import { Breadcrumbs, LayersIcon } from "@plane/ui"; // components -import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; +import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // types import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const ProjectArchivedIssuesHeader: FC = observer(() => { // router @@ -91,13 +91,9 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 139ec0257..4f2929621 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -1,18 +1,18 @@ import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; -// ui import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +// ui // helper -import { renderEmoji } from "helpers/emoji.helper"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; export const ProjectDraftIssueHeader: FC = observer(() => { // router @@ -86,13 +86,9 @@ export const ProjectDraftIssueHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx index b5260edd7..0e1bdcd1e 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/components/headers/project-inbox.tsx @@ -1,17 +1,17 @@ import { FC, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Plus } from "lucide-react"; // hooks -import { useProject } from "hooks/store"; // ui import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; // components -import { CreateInboxIssueModal } from "components/inbox"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { CreateInboxIssueModal } from "components/inbox"; // helper -import { renderEmoji } from "helpers/emoji.helper"; +import { useProject } from "hooks/store"; +import { ProjectLogo } from "components/project"; export const ProjectInboxHeader: FC = observer(() => { // states @@ -35,13 +35,9 @@ export const ProjectInboxHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-issue-details.tsx b/web/components/headers/project-issue-details.tsx index 3732f2598..080a34560 100644 --- a/web/components/headers/project-issue-details.tsx +++ b/web/components/headers/project-issue-details.tsx @@ -1,41 +1,32 @@ import { FC } from "react"; -import useSWR from "swr"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useApplication, useProject } from "hooks/store"; -// ui -import { Breadcrumbs, LayersIcon } from "@plane/ui"; -// helpers -import { renderEmoji } from "helpers/emoji.helper"; -// services -import { IssueService } from "services/issue"; -// constants -import { ISSUE_DETAILS } from "constants/fetch-keys"; -// components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; import { PanelRight } from "lucide-react"; +import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { cn } from "helpers/common.helper"; - +import { useApplication, useIssueDetail, useProject } from "hooks/store"; +// ui +// helpers // services -const issueService = new IssueService(); +import { ProjectLogo } from "components/project"; +// constants +// components export const ProjectIssueDetailsHeader: FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; // store hooks - const { currentProjectDetails, getProjectById } = useProject(); + const { currentProjectDetails } = useProject(); const { theme: themeStore } = useApplication(); - - const { data: issueDetails } = useSWR( - workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, - workspaceSlug && projectId && issueId - ? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string) - : null - ); - + const { + issue: { getIssueById }, + } = useIssueDetail(); + // derived values + const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined; const isSidebarCollapsed = themeStore.issueDetailSidebarCollapsed; return ( @@ -51,13 +42,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => { href={`/${workspaceSlug}/projects`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } @@ -81,8 +68,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => { link={ } @@ -91,7 +79,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
); diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 43030c5c2..19eaf4f4f 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -1,8 +1,16 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; // hooks +import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; +import { ProjectAnalyticsModal } from "components/analytics"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useApplication, useEventTracker, @@ -12,22 +20,14 @@ import { useUser, useMember, } from "hooks/store"; +import { useIssues } from "hooks/store/use-issues"; // components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { ProjectAnalyticsModal } from "components/analytics"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { ProjectLogo } from "components/project"; // constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; // helper -import { renderEmoji } from "helpers/emoji.helper"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues } from "hooks/store/use-issues"; -import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; export const ProjectIssuesHeader: React.FC = observer(() => { // states @@ -123,17 +123,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => { label={currentProjectDetails?.name ?? "Project"} icon={ currentProjectDetails ? ( - currentProjectDetails?.emoji ? ( - - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) ) : ( diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx index b70a4614f..817d842b4 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/components/headers/project-settings.tsx @@ -1,17 +1,17 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // ui import { Breadcrumbs, CustomMenu } from "@plane/ui"; // helper -import { renderEmoji } from "helpers/emoji.helper"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; // hooks import { useProject, useUser } from "hooks/store"; +import { ProjectLogo } from "components/project"; // constants -import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; export interface IProjectSettingHeader { title: string; @@ -44,13 +44,9 @@ export const ProjectSettingHeader: FC = observer((props) href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - renderEmoji(currentProjectDetails.emoji) - ) : currentProjectDetails?.icon_prop ? ( - renderEmoji(currentProjectDetails.icon_prop) - ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 175534a79..ab3959716 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -1,9 +1,21 @@ import { useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Plus } from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/router"; +import { Plus } from "lucide-react"; // hooks +// components +// ui +import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; +// helpers +// types +// constants +import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { truncateText } from "helpers/string.helper"; import { useApplication, useEventTracker, @@ -15,29 +27,13 @@ import { useProjectView, useUser, } from "hooks/store"; -// components -import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; -// ui -import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui"; -// helpers -import { truncateText } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; -// types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; -// constants -import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; +import { ProjectLogo } from "components/project"; export const ProjectViewIssuesHeader: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query as { - workspaceSlug: string; - projectId: string; - viewId: string; - }; + const { workspaceSlug, projectId, viewId } = router.query; // store hooks const { issuesFilter: { issueFilters, updateFilters }, @@ -61,15 +57,21 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, viewId); + if (!workspaceSlug || !projectId || !viewId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !viewId) return; const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { @@ -81,23 +83,41 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, viewId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, issueFilters, updateFilters] ); const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, viewId); + if (!workspaceSlug || !projectId || !viewId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + updatedDisplayFilter, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, viewId); + if (!workspaceSlug || !projectId || !viewId) return; + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_PROPERTIES, + property, + viewId.toString() + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); @@ -119,17 +139,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index bb070a22f..99533189a 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -1,16 +1,16 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Plus } from "lucide-react"; // hooks -import { useApplication, useProject, useUser } from "hooks/store"; // components import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; -// constants import { EUserProjectRoles } from "constants/project"; +// constants +import { useApplication, useProject, useUser } from "hooks/store"; +import { ProjectLogo } from "components/project"; export const ProjectViewsHeader: React.FC = observer(() => { // router @@ -42,17 +42,9 @@ export const ProjectViewsHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} label={currentProjectDetails?.name ?? "Project"} icon={ - currentProjectDetails?.emoji ? ( - - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} + currentProjectDetails && ( + + ) } diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index f6dd7fd3c..3810860aa 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react-lite"; import { Search, Plus, Briefcase } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // constants +import { BreadcrumbLink } from "components/common"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { EUserWorkspaceRoles } from "constants/workspace"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { BreadcrumbLink } from "components/common"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; export const ProjectsHeader = observer(() => { // store hooks diff --git a/web/components/headers/user-profile.tsx b/web/components/headers/user-profile.tsx index 30bc5b2a9..09b764cdc 100644 --- a/web/components/headers/user-profile.tsx +++ b/web/components/headers/user-profile.tsx @@ -1,23 +1,23 @@ // ui +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { ChevronDown, PanelRight } from "lucide-react"; import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; // components import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -import { cn } from "helpers/common.helper"; -import { FC } from "react"; -import { useApplication, useUser } from "hooks/store"; -import { ChevronDown, PanelRight } from "lucide-react"; -import { observer } from "mobx-react-lite"; import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile"; -import Link from "next/link"; -import { useRouter } from "next/router"; +import { cn } from "helpers/common.helper"; +import { useApplication, useUser } from "hooks/store"; type TUserProfileHeader = { - type?: string | undefined -} + type?: string | undefined; +}; export const UserProfileHeader: FC = observer((props) => { - const { type = undefined } = props + const { type = undefined } = props; const router = useRouter(); const { workspaceSlug, userId } = router.query; @@ -34,45 +34,60 @@ export const UserProfileHeader: FC = observer((props) => { const { theme: themStore } = useApplication(); - return (
-
- -
- - } /> - -
- - {type} - -
- } - customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" - closeOnSelect - > - <> - {tabsList.map((tab) => ( - - {tab.label} - - ))} - - + return ( +
+
+ +
+ + } + /> + +
+ + {type} + +
+ } + customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" + closeOnSelect + > + <> + {tabsList.map((tab) => ( + + + {tab.label} + + + ))} + + +
-
) + ); }); - - diff --git a/web/components/headers/workspace-active-cycles.tsx b/web/components/headers/workspace-active-cycles.tsx index 195b89471..a33161de9 100644 --- a/web/components/headers/workspace-active-cycles.tsx +++ b/web/components/headers/workspace-active-cycles.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react-lite"; // ui +import { Crown } from "lucide-react"; import { Breadcrumbs, ContrastIcon } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // icons -import { Crown } from "lucide-react"; export const WorkspaceActiveCycleHeader = observer(() => (
diff --git a/web/components/headers/workspace-analytics.tsx b/web/components/headers/workspace-analytics.tsx index a6ad67f05..2bede32ba 100644 --- a/web/components/headers/workspace-analytics.tsx +++ b/web/components/headers/workspace-analytics.tsx @@ -1,14 +1,14 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { BarChart2, PanelRight } from "lucide-react"; // ui import { Breadcrumbs } from "@plane/ui"; // components -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; -import { useApplication } from "hooks/store"; -import { observer } from "mobx-react"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { cn } from "helpers/common.helper"; -import { useEffect } from "react"; +import { useApplication } from "hooks/store"; export const WorkspaceAnalyticsHeader = observer(() => { const router = useRouter(); @@ -47,11 +47,21 @@ export const WorkspaceAnalyticsHeader = observer(() => { } /> - {analytics_tab === 'custom' && - - } + )}
diff --git a/web/components/headers/workspace-dashboard.tsx b/web/components/headers/workspace-dashboard.tsx index 6b85577f6..e7ae3c726 100644 --- a/web/components/headers/workspace-dashboard.tsx +++ b/web/components/headers/workspace-dashboard.tsx @@ -1,17 +1,17 @@ -import { LayoutGrid, Zap } from "lucide-react"; import Image from "next/image"; import { useTheme } from "next-themes"; +import { LayoutGrid, Zap } from "lucide-react"; // images import githubBlackImage from "/public/logos/github-black.png"; import githubWhiteImage from "/public/logos/github-white.png"; // hooks -import { useEventTracker } from "hooks/store"; // components -import { BreadcrumbLink } from "components/common"; import { Breadcrumbs } from "@plane/ui"; +import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; // constants import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker"; +import { useEventTracker } from "hooks/store"; export const WorkspaceDashboardHeader = () => { // hooks diff --git a/web/components/headers/workspace-settings.tsx b/web/components/headers/workspace-settings.tsx index 5ced55204..faf1a45d1 100644 --- a/web/components/headers/workspace-settings.tsx +++ b/web/components/headers/workspace-settings.tsx @@ -1,14 +1,14 @@ import { FC } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // ui -import { Breadcrumbs, CustomMenu } from "@plane/ui"; import { Settings } from "lucide-react"; +import { Breadcrumbs, CustomMenu } from "@plane/ui"; // hooks -import { observer } from "mobx-react-lite"; // components +import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; -import { BreadcrumbLink } from "components/common"; export interface IWorkspaceSettingHeader { title: string; diff --git a/web/components/icons/priority-icon.tsx b/web/components/icons/priority-icon.tsx index b23f56eab..36ea67b3d 100644 --- a/web/components/icons/priority-icon.tsx +++ b/web/components/icons/priority-icon.tsx @@ -14,12 +14,12 @@ export const PriorityIcon: React.FC = ({ priority, className = "" }) => { {priority === "urgent" ? "error" : priority === "high" - ? "signal_cellular_alt" - : priority === "medium" - ? "signal_cellular_alt_2_bar" - : priority === "low" - ? "signal_cellular_alt_1_bar" - : "block"} + ? "signal_cellular_alt" + : priority === "medium" + ? "signal_cellular_alt_2_bar" + : priority === "low" + ? "signal_cellular_alt_1_bar" + : "block"} ); }; diff --git a/web/components/icons/state/state-group-icon.tsx b/web/components/icons/state/state-group-icon.tsx index 15debf5f2..ae9e5f1a9 100644 --- a/web/components/icons/state/state-group-icon.tsx +++ b/web/components/icons/state/state-group-icon.tsx @@ -7,9 +7,9 @@ import { StateGroupUnstartedIcon, } from "components/icons"; // types +import { STATE_GROUPS } from "constants/state"; import { TStateGroups } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; type Props = { className?: string; diff --git a/web/components/inbox/content/root.tsx b/web/components/inbox/content/root.tsx index 26f58131e..7cc19bec3 100644 --- a/web/components/inbox/content/root.tsx +++ b/web/components/inbox/content/root.tsx @@ -2,12 +2,12 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { Inbox } from "lucide-react"; // hooks -import { useInboxIssues } from "hooks/store"; -// components +import { Loader } from "@plane/ui"; import { InboxIssueActionsHeader } from "components/inbox"; import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox"; +import { useInboxIssues } from "hooks/store"; +// components // ui -import { Loader } from "@plane/ui"; type TInboxContentRoot = { workspaceSlug: string; diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 8a3bb4261..661bc2d72 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -1,11 +1,12 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { DayPicker } from "react-day-picker"; import { Popover } from "@headlessui/react"; -// hooks -import { useUser, useInboxIssues, useIssueDetail, useWorkspace, useEventTracker } from "hooks/store"; -import useToast from "hooks/use-toast"; +// icons +import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; +// ui +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // components import { AcceptIssueModal, @@ -13,14 +14,12 @@ import { DeleteInboxIssueModal, SelectDuplicateInboxIssueModal, } from "components/inbox"; -// ui -import { Button } from "@plane/ui"; -// icons -import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; -// types -import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types"; -import { EUserProjectRoles } from "constants/project"; import { ISSUE_DELETED } from "constants/event-tracker"; +import { EUserProjectRoles } from "constants/project"; +// hooks +import { useUser, useInboxIssues, useIssueDetail, useWorkspace, useEventTracker } from "hooks/store"; +// types +import type { TInboxDetailedStatus } from "@plane/types"; type TInboxIssueActionsHeader = { workspaceSlug: string; @@ -30,7 +29,7 @@ type TInboxIssueActionsHeader = { }; type TInboxIssueOperations = { - updateInboxIssueStatus: (data: TInboxStatus) => Promise; + updateInboxIssueStatus: (data: TInboxDetailedStatus) => Promise; removeInboxIssue: () => Promise; }; @@ -51,7 +50,6 @@ export const InboxIssueActionsHeader: FC = observer((p currentUser, membership: { currentProjectRole }, } = useUser(); - const { setToastAlert } = useToast(); // states const [date, setDate] = useState(new Date()); @@ -74,8 +72,8 @@ export const InboxIssueActionsHeader: FC = observer((p if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) throw new Error("Missing required parameters"); await updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while updating inbox status. Please try again.", }); @@ -98,8 +96,8 @@ export const InboxIssueActionsHeader: FC = observer((p pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while deleting inbox issue. Please try again.", }); @@ -122,7 +120,6 @@ export const InboxIssueActionsHeader: FC = observer((p inboxIssueId, updateInboxIssueStatus, removeInboxIssue, - setToastAlert, captureIssueEvent, router, ] @@ -131,6 +128,8 @@ export const InboxIssueActionsHeader: FC = observer((p const handleInboxIssueNavigation = useCallback( (direction: "next" | "prev") => { if (!inboxIssues || !inboxIssueId) return; + const activeElement = document.activeElement as HTMLElement; + if (activeElement && (activeElement.classList.contains("tiptap") || activeElement.id === "title-input")) return; const nextIssueIndex = direction === "next" ? (currentIssueIndex + 1) % inboxIssues.length @@ -233,7 +232,7 @@ export const InboxIssueActionsHeader: FC = observer((p )} {inboxIssueId && ( -
+
diff --git a/web/components/issues/attachment/attachment-detail.tsx b/web/components/issues/attachment/attachment-detail.tsx index 0d345a619..8ff2b9305 100644 --- a/web/components/issues/attachment/attachment-detail.tsx +++ b/web/components/issues/attachment/attachment-detail.tsx @@ -2,17 +2,17 @@ import { FC, useState } from "react"; import Link from "next/link"; import { AlertCircle, X } from "lucide-react"; // hooks -import { useIssueDetail, useMember } from "hooks/store"; // ui import { Tooltip } from "@plane/ui"; // components -import { IssueAttachmentDeleteModal } from "./delete-attachment-confirmation-modal"; // icons import { getFileIcon } from "components/icons"; // helper -import { truncateText } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { truncateText } from "helpers/string.helper"; +import { useIssueDetail, useMember } from "hooks/store"; +import { IssueAttachmentDeleteModal } from "./delete-attachment-confirmation-modal"; // types import { TAttachmentOperations } from "./root"; diff --git a/web/components/issues/attachment/attachment-upload.tsx b/web/components/issues/attachment/attachment-upload.tsx index bf197980a..27dc572a9 100644 --- a/web/components/issues/attachment/attachment-upload.tsx +++ b/web/components/issues/attachment/attachment-upload.tsx @@ -2,11 +2,11 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; // hooks -import { useApplication } from "hooks/store"; // constants import { MAX_FILE_SIZE } from "constants/common"; // helpers import { generateFileName } from "helpers/attachment.helper"; +import { useApplication } from "hooks/store"; // types import { TAttachmentOperations } from "./root"; diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx index 2129a4f61..0f834c1a4 100644 --- a/web/components/issues/attachment/attachments-list.tsx +++ b/web/components/issues/attachment/attachments-list.tsx @@ -32,6 +32,7 @@ export const IssueAttachmentsList: FC = observer((props) issueAttachments.length > 0 && issueAttachments.map((attachmentId) => ( = (props) => { // hooks const { createAttachment, removeAttachment } = useIssueDetail(); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const handleAttachmentOperations: TAttachmentOperations = useMemo( () => ({ create: async (data: FormData) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); - const res = await createAttachment(workspaceSlug, projectId, issueId, data); - setToastAlert({ - message: "The attachment has been successfully uploaded", - type: "success", - title: "Attachment uploaded", + + const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, data); + setPromiseToast(attachmentUploadPromise, { + loading: "Uploading attachment...", + success: { + title: "Attachment uploaded", + message: () => "The attachment has been successfully uploaded", + }, + error: { + title: "Attachment not uploaded", + message: () => "The attachment could not be uploaded", + }, }); + + const res = await attachmentUploadPromise; captureIssueEvent({ eventName: "Issue attachment added", payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, @@ -50,20 +59,15 @@ export const IssueAttachmentRoot: FC = (props) => { eventName: "Issue attachment added", payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, }); - setToastAlert({ - message: "The attachment could not be uploaded", - type: "error", - title: "Attachment not uploaded", - }); } }, remove: async (attachmentId: string) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await removeAttachment(workspaceSlug, projectId, issueId, attachmentId); - setToastAlert({ + setToast({ message: "The attachment has been successfully removed", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Attachment removed", }); captureIssueEvent({ @@ -83,15 +87,15 @@ export const IssueAttachmentRoot: FC = (props) => { change_details: "", }, }); - setToastAlert({ + setToast({ message: "The Attachment could not be removed", - type: "error", + type: TOAST_TYPE.ERROR, title: "Attachment not removed", }); } }, }), - [workspaceSlug, projectId, issueId, createAttachment, removeAttachment, setToastAlert] + [workspaceSlug, projectId, issueId, createAttachment, removeAttachment] ); return ( diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index 3a9c0653e..b6c08c14b 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -2,13 +2,11 @@ import { useEffect, useState, Fragment } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // ui -import { Button } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // types -import { useIssues } from "hooks/store/use-issues"; import { TIssue } from "@plane/types"; -import { useProject } from "hooks/store"; +// hooks +import { useIssues, useProject } from "hooks/store"; type Props = { isOpen: boolean; @@ -25,7 +23,6 @@ export const DeleteIssueModal: React.FC = (props) => { const [isDeleting, setIsDeleting] = useState(false); - const { setToastAlert } = useToast(); // hooks const { getProjectById } = useProject(); @@ -50,9 +47,9 @@ export const DeleteIssueModal: React.FC = (props) => { onClose(); }) .catch(() => { - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: "Failed to delete issue", }); }) diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index c64c147ea..b13e124f3 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -1,18 +1,18 @@ import { ChangeEvent, FC, useCallback, useEffect, useState } from "react"; +import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; +import debounce from "lodash/debounce"; +import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; // hooks -import useReloadConfirmations from "hooks/use-reload-confirmation"; -import debounce from "lodash/debounce"; -// components import { Loader, TextArea } from "@plane/ui"; -import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; +import { useMention, useWorkspace } from "hooks/store"; +import useReloadConfirmations from "hooks/use-reload-confirmation"; +// components // types +import { FileService } from "services/file.service"; import { TIssue } from "@plane/types"; import { TIssueOperations } from "./issue-detail"; // services -import { FileService } from "services/file.service"; -import { useMention, useWorkspace } from "hooks/store"; -import { observer } from "mobx-react"; export interface IssueDescriptionFormValues { name: string; @@ -71,16 +71,10 @@ export const IssueDescriptionForm: FC = observer((props) => { async (formData: Partial) => { if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; - await issueOperations.update( - workspaceSlug, - projectId, - issueId, - { - name: formData.name ?? "", - description_html: formData.description_html ?? "

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

", + }); }, [workspaceSlug, projectId, issueId, issueOperations] ); @@ -142,7 +136,7 @@ export const IssueDescriptionForm: FC = observer((props) => { debouncedFormSave(); }} required - className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary" + className="block min-h-min w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary" hasError={Boolean(errors?.name)} role="textbox" /> @@ -179,7 +173,7 @@ export const IssueDescriptionForm: FC = observer((props) => { setIsSubmitting={setIsSubmitting} dragDropEnabled customClassName="min-h-[150px] shadow-sm" - onChange={(description: Object, description_html: string) => { + onChange={(description: any, description_html: string) => { setShowAlert(true); setIsSubmitting("submitting"); onChange(description_html); diff --git a/web/components/issues/description-input.tsx b/web/components/issues/description-input.tsx index 79634fa84..4f1f5c056 100644 --- a/web/components/issues/description-input.tsx +++ b/web/components/issues/description-input.tsx @@ -1,16 +1,15 @@ import { FC, useState, useEffect } from "react"; // components -import { Loader } from "@plane/ui"; import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; -// store hooks +import { Loader } from "@plane/ui"; +// hooks import { useMention, useWorkspace } from "hooks/store"; +import useDebounce from "hooks/use-debounce"; // services import { FileService } from "services/file.service"; const fileService = new FileService(); // types import { TIssueOperations } from "./issue-detail"; -// hooks -import useDebounce from "hooks/use-debounce"; export type IssueDescriptionInputProps = { workspaceSlug: string; @@ -41,11 +40,9 @@ export const IssueDescriptionInput: FC = (props) => useEffect(() => { if (debouncedValue && debouncedValue !== value) { - issueOperations - .update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }, false) - .finally(() => { - setIsSubmitting("submitted"); - }); + issueOperations.update(workspaceSlug, projectId, issueId, { description_html: debouncedValue }).finally(() => { + setIsSubmitting("submitted"); + }); } // DO NOT Add more dependencies here. It will cause multiple requests to be sent. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -80,7 +77,7 @@ export const IssueDescriptionInput: FC = (props) => initialValue={initialValue} dragDropEnabled customClassName="min-h-[150px] shadow-sm" - onChange={(description: Object, description_html: string) => { + onChange={(description: any, description_html: string) => { setIsSubmitting("submitting"); setDescriptionHTML(description_html === "" ? "

" : description_html); }} diff --git a/web/components/issues/issue-detail/cycle-select.tsx b/web/components/issues/issue-detail/cycle-select.tsx index fb8449d6f..8744857c1 100644 --- a/web/components/issues/issue-detail/cycle-select.tsx +++ b/web/components/issues/issue-detail/cycle-select.tsx @@ -1,13 +1,12 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail } from "hooks/store"; // components import { CycleDropdown } from "components/dropdowns"; // ui -import { Spinner } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; +import { useIssueDetail } from "hooks/store"; // types import type { TIssueOperations } from "./root"; @@ -41,22 +40,21 @@ export const IssueCycleSelect: React.FC = observer((props) => }; return ( -
+
- {isUpdating && }
); }); diff --git a/web/components/issues/issue-detail/inbox/index.ts b/web/components/issues/issue-detail/inbox/index.ts index 97c28cc7c..0c4adc7d0 100644 --- a/web/components/issues/issue-detail/inbox/index.ts +++ b/web/components/issues/issue-detail/inbox/index.ts @@ -1,3 +1,3 @@ -export * from "./root" -export * from "./main-content" -export * from "./sidebar" \ No newline at end of file +export * from "./root"; +export * from "./main-content"; +export * from "./sidebar"; diff --git a/web/components/issues/issue-detail/inbox/main-content.tsx b/web/components/issues/issue-detail/inbox/main-content.tsx index d753be02f..f2aa78ad9 100644 --- a/web/components/issues/issue-detail/inbox/main-content.tsx +++ b/web/components/issues/issue-detail/inbox/main-content.tsx @@ -1,17 +1,17 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { StateGroupIcon } from "@plane/ui"; +import { IssueUpdateStatus, TIssueOperations } from "components/issues"; import { useIssueDetail, useProjectState, useUser } from "hooks/store"; import useReloadConfirmations from "hooks/use-reload-confirmation"; // components -import { IssueUpdateStatus, TIssueOperations } from "components/issues"; -import { IssueTitleInput } from "../../title-input"; -import { IssueDescriptionInput } from "../../description-input"; -import { IssueReaction } from "../reactions"; -import { IssueActivity } from "../issue-activity"; import { InboxIssueStatus } from "../../../inbox/inbox-issue-status"; +import { IssueDescriptionInput } from "../../description-input"; +import { IssueTitleInput } from "../../title-input"; +import { IssueActivity } from "../issue-activity"; +import { IssueReaction } from "../reactions"; // ui -import { StateGroupIcon } from "@plane/ui"; type Props = { workspaceSlug: string; @@ -65,7 +65,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => { projectId={projectId} inboxId={inboxId} issueId={issueId} - showDescription={true} + showDescription />
diff --git a/web/components/issues/issue-detail/inbox/root.tsx b/web/components/issues/issue-detail/inbox/root.tsx index d96b36efa..144198085 100644 --- a/web/components/issues/issue-detail/inbox/root.tsx +++ b/web/components/issues/issue-detail/inbox/root.tsx @@ -2,16 +2,16 @@ import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // components -import { InboxIssueMainContent } from "./main-content"; -import { InboxIssueDetailsSidebar } from "./sidebar"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { EUserProjectRoles } from "constants/project"; import { useEventTracker, useInboxIssues, useIssueDetail, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui // types import { TIssue } from "@plane/types"; import { TIssueOperations } from "../root"; +import { InboxIssueMainContent } from "./main-content"; +import { InboxIssueDetailsSidebar } from "./sidebar"; // constants -import { EUserProjectRoles } from "constants/project"; export type TInboxIssueDetailRoot = { workspaceSlug: string; @@ -34,7 +34,6 @@ export const InboxIssueDetailRoot: FC = (props) => { fetchComments, } = useIssueDetail(); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const { membership: { currentProjectRole }, } = useUser(); @@ -48,22 +47,9 @@ export const InboxIssueDetailRoot: FC = (props) => { console.error("Error fetching the parent issue"); } }, - update: async ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast: boolean = true - ) => { + update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { try { await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data); - if (showToast) { - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); - } captureIssueEvent({ eventName: "Inbox issue updated", payload: { ...data, state: "SUCCESS", element: "Inbox" }, @@ -74,9 +60,9 @@ export const InboxIssueDetailRoot: FC = (props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue update failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue update failed", }); captureIssueEvent({ @@ -93,9 +79,9 @@ export const InboxIssueDetailRoot: FC = (props) => { remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await removeInboxIssue(workspaceSlug, projectId, inboxId, issueId); - setToastAlert({ + setToast({ title: "Issue deleted successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Issue deleted successfully", }); captureIssueEvent({ @@ -109,15 +95,15 @@ export const InboxIssueDetailRoot: FC = (props) => { payload: { id: issueId, state: "FAILED", element: "Inbox" }, path: router.asPath, }); - setToastAlert({ + setToast({ title: "Issue delete failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue delete failed", }); } }, }), - [inboxId, fetchInboxIssueById, updateInboxIssue, removeInboxIssue, setToastAlert] + [inboxId, fetchInboxIssueById, updateInboxIssue, removeInboxIssue] ); useSWR( diff --git a/web/components/issues/issue-detail/inbox/sidebar.tsx b/web/components/issues/issue-detail/inbox/sidebar.tsx index 592791a85..bf9e833ce 100644 --- a/web/components/issues/issue-detail/inbox/sidebar.tsx +++ b/web/components/issues/issue-detail/inbox/sidebar.tsx @@ -2,14 +2,14 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { CalendarCheck2, Signal, Tag } from "lucide-react"; // hooks -import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // components -import { IssueLabel, TIssueOperations } from "components/issues"; -import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; -// icons import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; +import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; +import { IssueLabel, TIssueOperations } from "components/issues"; +// icons // helper import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; type Props = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx index 575e8d841..af3266067 100644 --- a/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -31,6 +31,7 @@ export const IssueActivityCommentRoot: FC = observer( {activityComments.map((activityComment, index) => activityComment.activity_type === "COMMENT" ? ( = observer((props > <> {activity.old_value === "" ? `added a new assignee ` : `removed the assignee `} - = observer((props > {activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value} - {showIssue && (activity.old_value === "" ? ` to ` : ` from `)} {showIssue && }. diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx index 8336e516f..ec3c777fc 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/cycle.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { ContrastIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; // icons -import { ContrastIcon } from "@plane/ui"; type TIssueCycleActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx index e45387535..0eeb7ecac 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/default.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { LayersIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; // icons -import { LayersIcon } from "@plane/ui"; type TIssueDefaultActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx index e01b94e1b..a8c309bd5 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/estimate.tsx @@ -33,13 +33,11 @@ export const IssueEstimateActivity: FC = observer((props {activity.new_value ? `set the estimate point to ` : `removed the estimate point `} {activity.new_value && ( <> - {areEstimatesEnabledForCurrentProject ? estimateValue : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} - )} {showIssue && (activity.new_value ? ` to ` : ` from `)} diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx index e209b4bbf..0097b65b6 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx @@ -1,13 +1,13 @@ import { FC, ReactNode } from "react"; import { Network } from "lucide-react"; // hooks +import { Tooltip } from "@plane/ui"; +import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // ui -import { Tooltip } from "@plane/ui"; // components import { IssueUser } from "../"; // helpers -import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; type TIssueActivityBlockComponent = { icon?: ReactNode; @@ -33,7 +33,7 @@ export const IssueActivityBlockComponent: FC = (pr ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2` }`} > -
+
{icon ? icon : }
diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx index e86b1fb57..49f813ec6 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // hooks +import { Tooltip } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // ui -import { Tooltip } from "@plane/ui"; type TIssueLink = { activityId: string; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx index c8089d233..0108c56b3 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/module.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { DiceIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; // icons -import { DiceIcon } from "@plane/ui"; type TIssueModuleActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx index e68a7c373..5ef67cf52 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/relation.tsx @@ -1,13 +1,13 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { issueRelationObject } from "components/issues/issue-detail/relation-select"; import { useIssueDetail } from "hooks/store"; // components +import { TIssueRelationTypes } from "@plane/types"; import { IssueActivityBlockComponent } from "./"; // component helpers -import { issueRelationObject } from "components/issues/issue-detail/relation-select"; // types -import { TIssueRelationTypes } from "@plane/types"; type TIssueRelationActivity = { activityId: string; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx index 95b3cda80..0e3a80b34 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx @@ -2,11 +2,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { CalendarDays } from "lucide-react"; // hooks +import { renderFormattedDate } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; type TIssueStartDateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx index 7cc47c2c8..757519388 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/state.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; // hooks +import { DoubleCircleIcon } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; // icons -import { DoubleCircleIcon } from "@plane/ui"; type TIssueStateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx index a4b40ec31..947b2e6e6 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx @@ -2,11 +2,11 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { CalendarDays } from "lucide-react"; // hooks +import { renderFormattedDate } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; // helpers -import { renderFormattedDate } from "helpers/date-time.helper"; type TIssueTargetDateActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; diff --git a/web/components/issues/issue-detail/issue-activity/activity/root.tsx b/web/components/issues/issue-detail/issue-activity/activity/root.tsx index af44463d5..092633b06 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/root.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/root.tsx @@ -23,6 +23,7 @@ export const IssueActivityRoot: FC = observer((props) => {
{activityIds.map((activityId, index) => ( diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx index 4dbc36f6b..b00dd2a13 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-block.tsx @@ -1,9 +1,9 @@ import { FC, ReactNode } from "react"; import { MessageCircle } from "lucide-react"; // hooks +import { calculateTimeAgo } from "helpers/date-time.helper"; import { useIssueDetail } from "hooks/store"; // helpers -import { calculateTimeAgo } from "helpers/date-time.helper"; type TIssueCommentBlock = { commentId: string; @@ -24,7 +24,7 @@ export const IssueCommentBlock: FC = (props) => { if (!comment) return <>; return (
-
+
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? ( = (props) => { value={watch("comment_html") ?? ""} debouncedUpdatesEnabled={false} customClassName="min-h-[50px] p-3 shadow-sm" - onChange={(comment_json: Object, comment_html: string) => setValue("comment_html", comment_html)} + onChange={(comment_json: any, comment_html: string) => setValue("comment_html", comment_html)} mentionSuggestions={mentionSuggestions} mentionHighlights={mentionHighlights} /> @@ -150,7 +150,7 @@ export const IssueCommentCard: FC = (props) => { onClick={handleSubmit(onEnter)} disabled={isSubmitting || isEmpty} className={`group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 ${ - isEmpty ? "bg-gray-200 cursor-not-allowed" : "hover:bg-green-500" + isEmpty ? "cursor-not-allowed bg-gray-200" : "hover:bg-green-500" }`} > = (props) => { return (
{ - if (e.key === "Enter" && !e.shiftKey && !isEmpty) { + if (e.key === "Enter" && !e.shiftKey && !isEmpty && !isSubmitting) { handleSubmit(onSubmit)(e); } }} @@ -97,7 +97,7 @@ export const IssueCommentCreate: FC = (props) => { customClassName="p-2" editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} - onChange={(comment_json: Object, comment_html: string) => { + onChange={(comment_json: any, comment_html: string) => { onChange(comment_html); }} mentionSuggestions={mentionSuggestions} diff --git a/web/components/issues/issue-detail/issue-activity/comments/root.tsx b/web/components/issues/issue-detail/issue-activity/comments/root.tsx index 4e2775c4a..0696fa129 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/root.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/root.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail } from "hooks/store"; // components +import { TActivityOperations } from "../root"; import { IssueCommentCard } from "./comment-card"; // types -import { TActivityOperations } from "../root"; type TIssueCommentRoot = { workspaceSlug: string; @@ -28,6 +28,7 @@ export const IssueCommentRoot: FC = observer((props) => {
{commentIds.map((commentId, index) => ( = observer((props) => { const { workspaceSlug, projectId, issueId } = props; // hooks const { createComment, updateComment, removeComment } = useIssueDetail(); - const { setToastAlert } = useToast(); const { getProjectById } = useProject(); // state const [activityTab, setActivityTab] = useState("all"); @@ -56,15 +56,15 @@ export const IssueActivity: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await createComment(workspaceSlug, projectId, issueId, data); - setToastAlert({ + setToast({ title: "Comment created successfully.", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Comment created successfully.", }); } catch (error) { - setToastAlert({ + setToast({ title: "Comment creation failed.", - type: "error", + type: TOAST_TYPE.ERROR, message: "Comment creation failed. Please try again later.", }); } @@ -73,15 +73,15 @@ export const IssueActivity: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await updateComment(workspaceSlug, projectId, issueId, commentId, data); - setToastAlert({ + setToast({ title: "Comment updated successfully.", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Comment updated successfully.", }); } catch (error) { - setToastAlert({ + setToast({ title: "Comment update failed.", - type: "error", + type: TOAST_TYPE.ERROR, message: "Comment update failed. Please try again later.", }); } @@ -90,21 +90,21 @@ export const IssueActivity: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await removeComment(workspaceSlug, projectId, issueId, commentId); - setToastAlert({ + setToast({ title: "Comment removed successfully.", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Comment removed successfully.", }); } catch (error) { - setToastAlert({ + setToast({ title: "Comment remove failed.", - type: "error", + type: TOAST_TYPE.ERROR, message: "Comment remove failed. Please try again later.", }); } }, }), - [workspaceSlug, projectId, issueId, createComment, updateComment, removeComment, setToastAlert] + [workspaceSlug, projectId, issueId, createComment, updateComment, removeComment] ); const project = getProjectById(projectId); diff --git a/web/components/issues/issue-detail/label/create-label.tsx b/web/components/issues/issue-detail/label/create-label.tsx index 72bc034f8..68b86a86c 100644 --- a/web/components/issues/issue-detail/label/create-label.tsx +++ b/web/components/issues/issue-detail/label/create-label.tsx @@ -1,16 +1,15 @@ import { FC, useState, Fragment, useEffect } from "react"; -import { Plus, X, Loader } from "lucide-react"; -import { Controller, useForm } from "react-hook-form"; import { TwitterPicker } from "react-color"; +import { Controller, useForm } from "react-hook-form"; import { Popover, Transition } from "@headlessui/react"; +import { Plus, X, Loader } from "lucide-react"; // hooks +import { Input, TOAST_TYPE, setToast } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Input } from "@plane/ui"; // types -import { TLabelOperations } from "./root"; import { IIssueLabel } from "@plane/types"; +import { TLabelOperations } from "./root"; type ILabelCreate = { workspaceSlug: string; @@ -28,7 +27,6 @@ const defaultValues: Partial = { export const LabelCreate: FC = (props) => { const { workspaceSlug, projectId, issueId, labelOperations, disabled = false } = props; // hooks - const { setToastAlert } = useToast(); const { issue: { getIssueById }, } = useIssueDetail(); @@ -63,9 +61,9 @@ export const LabelCreate: FC = (props) => { await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels }); reset(defaultValues); } catch (error) { - setToastAlert({ + setToast({ title: "Label creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Label creation failed. Please try again sometime later.", }); } @@ -74,7 +72,7 @@ export const LabelCreate: FC = (props) => { return ( <>
@@ -151,7 +149,7 @@ export const LabelCreate: FC = (props) => { )} diff --git a/web/components/issues/issue-detail/label/label-list-item.tsx b/web/components/issues/issue-detail/label/label-list-item.tsx index 60792b01c..69c0e08e9 100644 --- a/web/components/issues/issue-detail/label/label-list-item.tsx +++ b/web/components/issues/issue-detail/label/label-list-item.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; import { X } from "lucide-react"; // types -import { TLabelOperations } from "./root"; import { useIssueDetail, useLabel } from "hooks/store"; +import { TLabelOperations } from "./root"; type TLabelListItem = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/label/label-list.tsx b/web/components/issues/issue-detail/label/label-list.tsx index fd714e002..fdf94be28 100644 --- a/web/components/issues/issue-detail/label/label-list.tsx +++ b/web/components/issues/issue-detail/label/label-list.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // components +import { useIssueDetail } from "hooks/store"; import { LabelListItem } from "./label-list-item"; // hooks -import { useIssueDetail } from "hooks/store"; // types import { TLabelOperations } from "./root"; @@ -29,6 +29,7 @@ export const LabelList: FC = (props) => { <> {issueLabels.map((labelId) => ( = observer((props) => { // hooks const { updateIssue } = useIssueDetail(); const { createLabel } = useLabel(); - const { setToastAlert } = useToast(); const labelOperations: TLabelOperations = useMemo( () => ({ @@ -35,16 +35,10 @@ export const IssueLabel: FC = observer((props) => { try { if (onLabelUpdate) onLabelUpdate(data.label_ids || []); else await updateIssue(workspaceSlug, projectId, issueId, data); - if (!isInboxIssue) - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue update failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue update failed", }); } @@ -53,23 +47,23 @@ export const IssueLabel: FC = observer((props) => { try { const labelResponse = await createLabel(workspaceSlug, projectId, data); if (!isInboxIssue) - setToastAlert({ + setToast({ title: "Label created successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Label created successfully", }); return labelResponse; } catch (error) { - setToastAlert({ + setToast({ title: "Label creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Label creation failed", }); return error; } }, }), - [updateIssue, createLabel, setToastAlert, onLabelUpdate] + [updateIssue, createLabel, onLabelUpdate] ); return ( diff --git a/web/components/issues/issue-detail/label/select/label-select.tsx b/web/components/issues/issue-detail/label/select/label-select.tsx index 11ff77d37..844a17b79 100644 --- a/web/components/issues/issue-detail/label/select/label-select.tsx +++ b/web/components/issues/issue-detail/label/select/label-select.tsx @@ -1,11 +1,11 @@ import { Fragment, useState } from "react"; import { observer } from "mobx-react-lite"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, Search, Tag } from "lucide-react"; // hooks import { useIssueDetail, useLabel } from "hooks/store"; // components -import { Combobox } from "@headlessui/react"; export interface IIssueLabelSelect { workspaceSlug: string; @@ -24,7 +24,7 @@ export const IssueLabelSelect: React.FC = observer((props) => // states const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [query, setQuery] = useState(""); const issue = getIssueById(issueId); @@ -71,7 +71,7 @@ export const IssueLabelSelect: React.FC = observer((props) => const label = (
@@ -102,7 +102,7 @@ export const IssueLabelSelect: React.FC = observer((props) =>
-
+
{isLoading ? (

Loading...

) : filteredOptions.length > 0 ? ( diff --git a/web/components/issues/issue-detail/label/select/root.tsx b/web/components/issues/issue-detail/label/select/root.tsx index c31e1bc61..de0bcca90 100644 --- a/web/components/issues/issue-detail/label/select/root.tsx +++ b/web/components/issues/issue-detail/label/select/root.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; // components +import { TLabelOperations } from "../root"; import { IssueLabelSelect } from "./label-select"; // types -import { TLabelOperations } from "../root"; type TIssueLabelSelectRoot = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/links/create-update-link-modal.tsx b/web/components/issues/issue-detail/links/create-update-link-modal.tsx index fc9eb3838..689968f07 100644 --- a/web/components/issues/issue-detail/links/create-update-link-modal.tsx +++ b/web/components/issues/issue-detail/links/create-update-link-modal.tsx @@ -152,8 +152,8 @@ export const IssueLinkCreateUpdateModal: FC = (props) ? "Updating Link..." : "Update Link" : isSubmitting - ? "Adding Link..." - : "Add Link"} + ? "Adding Link..." + : "Add Link"}
diff --git a/web/components/issues/issue-detail/links/link-detail.tsx b/web/components/issues/issue-detail/links/link-detail.tsx index 6c37f86f9..4504329f0 100644 --- a/web/components/issues/issue-detail/links/link-detail.tsx +++ b/web/components/issues/issue-detail/links/link-detail.tsx @@ -1,16 +1,15 @@ import { FC, useState } from "react"; // hooks -import useToast from "hooks/use-toast"; -import { useIssueDetail, useMember } from "hooks/store"; // ui -import { ExternalLinkIcon, Tooltip } from "@plane/ui"; -// icons import { Pencil, Trash2, LinkIcon } from "lucide-react"; +import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// icons // types -import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; +import { useIssueDetail, useMember } from "hooks/store"; +import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal"; export type TIssueLinkDetail = { linkId: string; @@ -27,7 +26,6 @@ export const IssueLinkDetail: FC = (props) => { link: { getLinkById }, } = useIssueDetail(); const { getUserDetails } = useMember(); - const { setToastAlert } = useToast(); // state const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false); @@ -52,11 +50,11 @@ export const IssueLinkDetail: FC = (props) => {
{ copyTextToClipboard(linkDetail.url); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied!", message: "Link copied to clipboard", }); diff --git a/web/components/issues/issue-detail/links/links.tsx b/web/components/issues/issue-detail/links/links.tsx index 368bddb91..1120c3a5c 100644 --- a/web/components/issues/issue-detail/links/links.tsx +++ b/web/components/issues/issue-detail/links/links.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // computed +import { useIssueDetail, useUser } from "hooks/store"; import { IssueLinkDetail } from "./link-detail"; // hooks -import { useIssueDetail, useUser } from "hooks/store"; import { TLinkOperations } from "./root"; export type TLinkOperationsModal = Exclude; @@ -34,6 +34,7 @@ export const IssueLinkList: FC = observer((props) => { issueLinks.length > 0 && issueLinks.map((linkId) => ( ) => Promise; @@ -37,24 +38,22 @@ export const IssueLinkRoot: FC = (props) => { [toggleIssueLinkModalStore] ); - const { setToastAlert } = useToast(); - const handleLinkOperations: TLinkOperations = useMemo( () => ({ create: async (data: Partial) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await createLink(workspaceSlug, projectId, issueId, data); - setToastAlert({ + setToast({ message: "The link has been successfully created", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Link created", }); toggleIssueLinkModal(false); } catch (error) { - setToastAlert({ + setToast({ message: "The link could not be created", - type: "error", + type: TOAST_TYPE.ERROR, title: "Link not created", }); } @@ -63,16 +62,16 @@ export const IssueLinkRoot: FC = (props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await updateLink(workspaceSlug, projectId, issueId, linkId, data); - setToastAlert({ + setToast({ message: "The link has been successfully updated", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Link updated", }); toggleIssueLinkModal(false); } catch (error) { - setToastAlert({ + setToast({ message: "The link could not be updated", - type: "error", + type: TOAST_TYPE.ERROR, title: "Link not updated", }); } @@ -81,22 +80,22 @@ export const IssueLinkRoot: FC = (props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); await removeLink(workspaceSlug, projectId, issueId, linkId); - setToastAlert({ + setToast({ message: "The link has been successfully removed", - type: "success", + type: TOAST_TYPE.SUCCESS, title: "Link removed", }); toggleIssueLinkModal(false); } catch (error) { - setToastAlert({ + setToast({ message: "The link could not be removed", - type: "error", + type: TOAST_TYPE.ERROR, title: "Link not removed", }); } }, }), - [workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, setToastAlert, toggleIssueLinkModal] + [workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, toggleIssueLinkModal] ); return ( diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index 719129d98..b65560953 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -1,18 +1,18 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { StateGroupIcon } from "@plane/ui"; +import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; import { useIssueDetail, useProjectState, useUser } from "hooks/store"; import useReloadConfirmations from "hooks/use-reload-confirmation"; // components -import { IssueAttachmentRoot, IssueUpdateStatus } from "components/issues"; -import { IssueTitleInput } from "../title-input"; import { IssueDescriptionInput } from "../description-input"; +import { SubIssuesRoot } from "../sub-issues"; +import { IssueTitleInput } from "../title-input"; +import { IssueActivity } from "./issue-activity"; import { IssueParentDetail } from "./parent"; import { IssueReaction } from "./reactions"; -import { SubIssuesRoot } from "../sub-issues"; -import { IssueActivity } from "./issue-activity"; // ui -import { StateGroupIcon } from "@plane/ui"; // types import { TIssueOperations } from "./root"; diff --git a/web/components/issues/issue-detail/module-select.tsx b/web/components/issues/issue-detail/module-select.tsx index 41f4a06d6..f157ede86 100644 --- a/web/components/issues/issue-detail/module-select.tsx +++ b/web/components/issues/issue-detail/module-select.tsx @@ -1,14 +1,13 @@ import React, { useState } from "react"; -import { observer } from "mobx-react-lite"; import xor from "lodash/xor"; +import { observer } from "mobx-react-lite"; // hooks -import { useIssueDetail } from "hooks/store"; // components import { ModuleDropdown } from "components/dropdowns"; // ui -import { Spinner } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; +import { useIssueDetail } from "hooks/store"; // types import type { TIssueOperations } from "./root"; @@ -58,14 +57,14 @@ export const IssueModuleSelect: React.FC = observer((props) }; return ( -
+
= observer((props) showTooltip multiple /> - {isUpdating && }
); }); diff --git a/web/components/issues/issue-detail/parent-select.tsx b/web/components/issues/issue-detail/parent-select.tsx index 9a1aa48ad..0b6501027 100644 --- a/web/components/issues/issue-detail/parent-select.tsx +++ b/web/components/issues/issue-detail/parent-select.tsx @@ -1,15 +1,15 @@ import React from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { Pencil, X } from "lucide-react"; // hooks -import { useIssueDetail, useProject } from "hooks/store"; // components +import { Tooltip } from "@plane/ui"; import { ParentIssuesListModal } from "components/issues"; // ui -import { Tooltip } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; +import { useIssueDetail, useProject } from "hooks/store"; // types import { TIssueOperations } from "./root"; diff --git a/web/components/issues/issue-detail/parent/root.tsx b/web/components/issues/issue-detail/parent/root.tsx index a1c755e62..64d0d1182 100644 --- a/web/components/issues/issue-detail/parent/root.tsx +++ b/web/components/issues/issue-detail/parent/root.tsx @@ -2,14 +2,14 @@ import { FC } from "react"; import Link from "next/link"; import { MinusCircle } from "lucide-react"; // component -import { IssueParentSiblings } from "./siblings"; // ui import { CustomMenu } from "@plane/ui"; // hooks import { useIssues, useProject, useProjectState } from "hooks/store"; // types -import { TIssueOperations } from "../root"; import { TIssue } from "@plane/types"; +import { TIssueOperations } from "../root"; +import { IssueParentSiblings } from "./siblings"; export type TIssueParentDetail = { workspaceSlug: string; diff --git a/web/components/issues/issue-detail/parent/siblings.tsx b/web/components/issues/issue-detail/parent/siblings.tsx index 45eca81d4..b80a41327 100644 --- a/web/components/issues/issue-detail/parent/siblings.tsx +++ b/web/components/issues/issue-detail/parent/siblings.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import useSWR from "swr"; // components -import { IssueParentSiblingItem } from "./sibling-item"; // hooks import { useIssueDetail } from "hooks/store"; // types import { TIssue } from "@plane/types"; +import { IssueParentSiblingItem } from "./sibling-item"; export type TIssueParentSiblings = { currentIssue: TIssue; @@ -39,7 +39,9 @@ export const IssueParentSiblings: FC = (props) => { Loading
) : subIssueIds && subIssueIds.length > 0 ? ( - subIssueIds.map((issueId) => currentIssue.id != issueId && ) + subIssueIds.map( + (issueId) => currentIssue.id != issueId && + ) ) : (
No sibling issues diff --git a/web/components/issues/issue-detail/reactions/issue-comment.tsx b/web/components/issues/issue-detail/reactions/issue-comment.tsx index 30a8621e4..97c63a017 100644 --- a/web/components/issues/issue-detail/reactions/issue-comment.tsx +++ b/web/components/issues/issue-detail/reactions/issue-comment.tsx @@ -1,13 +1,13 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; // components -import { ReactionSelector } from "./reaction-selector"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { renderEmoji } from "helpers/emoji.helper"; import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui // types import { IUser } from "@plane/types"; -import { renderEmoji } from "helpers/emoji.helper"; +import { ReactionSelector } from "./reaction-selector"; export type TIssueCommentReaction = { workspaceSlug: string; @@ -25,7 +25,6 @@ export const IssueCommentReaction: FC = observer((props) createCommentReaction, removeCommentReaction, } = useIssueDetail(); - const { setToastAlert } = useToast(); const reactionIds = getCommentReactionsByCommentId(commentId); const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction); @@ -36,15 +35,15 @@ export const IssueCommentReaction: FC = observer((props) try { if (!workspaceSlug || !projectId || !commentId) throw new Error("Missing fields"); await createCommentReaction(workspaceSlug, projectId, commentId, reaction); - setToastAlert({ + setToast({ title: "Reaction created successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction created successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction creation failed", }); } @@ -53,15 +52,15 @@ export const IssueCommentReaction: FC = observer((props) try { if (!workspaceSlug || !projectId || !commentId || !currentUser?.id) throw new Error("Missing fields"); removeCommentReaction(workspaceSlug, projectId, commentId, reaction, currentUser.id); - setToastAlert({ + setToast({ title: "Reaction removed successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction removed successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction remove failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction remove failed", }); } @@ -71,16 +70,7 @@ export const IssueCommentReaction: FC = observer((props) else await issueCommentReactionOperations.create(reaction); }, }), - [ - workspaceSlug, - projectId, - commentId, - currentUser, - createCommentReaction, - removeCommentReaction, - setToastAlert, - userReactions, - ] + [workspaceSlug, projectId, commentId, currentUser, createCommentReaction, removeCommentReaction, userReactions] ); return ( diff --git a/web/components/issues/issue-detail/reactions/issue.tsx b/web/components/issues/issue-detail/reactions/issue.tsx index d6b33e36b..6f5610634 100644 --- a/web/components/issues/issue-detail/reactions/issue.tsx +++ b/web/components/issues/issue-detail/reactions/issue.tsx @@ -1,13 +1,13 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; // components -import { ReactionSelector } from "./reaction-selector"; -// hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { renderEmoji } from "helpers/emoji.helper"; import { useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui // types import { IUser } from "@plane/types"; -import { renderEmoji } from "helpers/emoji.helper"; +import { ReactionSelector } from "./reaction-selector"; export type TIssueReaction = { workspaceSlug: string; @@ -24,7 +24,6 @@ export const IssueReaction: FC = observer((props) => { createReaction, removeReaction, } = useIssueDetail(); - const { setToastAlert } = useToast(); const reactionIds = getReactionsByIssueId(issueId); const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction); @@ -35,15 +34,15 @@ export const IssueReaction: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); await createReaction(workspaceSlug, projectId, issueId, reaction); - setToastAlert({ + setToast({ title: "Reaction created successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction created successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction creation failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction creation failed", }); } @@ -52,15 +51,15 @@ export const IssueReaction: FC = observer((props) => { try { if (!workspaceSlug || !projectId || !issueId || !currentUser?.id) throw new Error("Missing fields"); await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id); - setToastAlert({ + setToast({ title: "Reaction removed successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Reaction removed successfully", }); } catch (error) { - setToastAlert({ + setToast({ title: "Reaction remove failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Reaction remove failed", }); } @@ -70,7 +69,7 @@ export const IssueReaction: FC = observer((props) => { else await issueReactionOperations.create(reaction); }, }), - [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, setToastAlert, userReactions] + [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions] ); return ( diff --git a/web/components/issues/issue-detail/reactions/reaction-selector.tsx b/web/components/issues/issue-detail/reactions/reaction-selector.tsx index 0782e7e15..655fd9105 100644 --- a/web/components/issues/issue-detail/reactions/reaction-selector.tsx +++ b/web/components/issues/issue-detail/reactions/reaction-selector.tsx @@ -1,9 +1,9 @@ import { Fragment } from "react"; import { Popover, Transition } from "@headlessui/react"; // helper +import { SmilePlus } from "lucide-react"; import { renderEmoji } from "helpers/emoji.helper"; // icons -import { SmilePlus } from "lucide-react"; const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"]; diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 67bba8697..0fd0902c6 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -1,16 +1,15 @@ import React from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; import { CircleDot, CopyPlus, Pencil, X, XCircle } from "lucide-react"; // hooks -import { useIssueDetail, useIssues, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components +import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; -// ui -import { RelatedIcon, Tooltip } from "@plane/ui"; -// helpers import { cn } from "helpers/common.helper"; +import { useIssueDetail, useIssues, useProject } from "hooks/store"; +// components +// ui +// helpers // types import { TIssueRelationTypes, ISearchIssueResponse } from "@plane/types"; @@ -60,15 +59,13 @@ export const IssueRelationSelect: React.FC = observer((pro toggleRelationModal, } = useIssueDetail(); const { issueMap } = useIssues(); - // toast alert - const { setToastAlert } = useToast(); const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey); const onSubmit = async (data: ISearchIssueResponse[]) => { if (data.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one issue.", }); @@ -102,7 +99,7 @@ export const IssueRelationSelect: React.FC = observer((pro
-
Properties
+
Properties
{/* TODO: render properties using a common component */} -
-
-
+
+
+
State
@@ -187,7 +199,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId?.toString() ?? ""} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName="text-sm" dropdownArrow @@ -195,8 +207,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
-
-
+
+
Assignees
@@ -208,7 +220,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { placeholder="Add assignees" multiple buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"} - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm justify-between ${ issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400" @@ -219,8 +231,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
-
-
+
+
Priority
@@ -235,8 +247,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
-
-
+
+
Start date
@@ -251,7 +263,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { maxDate={maxDate ?? undefined} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} hideIcon @@ -261,8 +273,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
-
-
+
+
Due date
@@ -277,7 +289,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { minDate={minDate ?? undefined} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={cn("text-sm", { "text-custom-text-400": !issue.target_date, @@ -291,8 +303,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
{areEstimatesEnabledForCurrentProject && ( -
-
+
+
Estimate
@@ -302,7 +314,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} disabled={!is_editable} buttonVariant="transparent-with-text" - className="w-3/5 flex-grow group" + className="group w-3/5 flex-grow" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`} placeholder="None" @@ -314,8 +326,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { )} {projectDetails?.module_view && ( -
-
+
+
Module
@@ -331,8 +343,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { )} {projectDetails?.cycle_view && ( -
-
+
+
Cycle
@@ -347,13 +359,13 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
)} -
-
+
+
Parent
= observer((props) => { />
-
-
+
+
Relates to
= observer((props) => { />
-
-
+
+
Blocking
= observer((props) => { />
-
-
+
+
Blocked by
= observer((props) => { />
-
-
+
+
Duplicate of
= observer((props) => { />
-
-
+
+
Labels
-
+
= observer((props) => { createSubscription, removeSubscription, } = useIssueDetail(); - const { setToastAlert } = useToast(); // state const [loading, setLoading] = useState(false); @@ -33,16 +31,16 @@ export const IssueSubscription: FC = observer((props) => { try { if (isSubscribed) await removeSubscription(workspaceSlug, projectId, issueId); else await createSubscription(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`, message: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`, }); setLoading(false); } catch (error) { setLoading(false); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: "Something went wrong. Please try again later.", }); diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 43f62e5be..8d2b56d2a 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -1,57 +1,55 @@ -import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; +import { FC } from "react"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // components +import { TOAST_TYPE, setToast } from "@plane/ui"; import { CalendarChart } from "components/issues"; // hooks -import useToast from "hooks/use-toast"; -// types -import { TGroupedIssues, TIssue } from "@plane/types"; -import { IQuickActionProps } from "../list/list-view-types"; -import { EIssueActions } from "../types"; -import { handleDragDrop } from "./utils"; import { useIssues, useUser } from "hooks/store"; -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; +import { useIssuesActions } from "hooks/use-issues-actions"; +// ui +// types +import { TGroupedIssues } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; +import { IQuickActionProps } from "../list/list-view-types"; +import { handleDragDrop } from "./utils"; import { EUserProjectRoles } from "constants/project"; +type CalendarStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW; + interface IBaseCalendarRoot { - issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues; - issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; + storeType: CalendarStoreType; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; } export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { - const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, isCompletedCycle = false } = props; + const { QuickActions, storeType, addIssuesToView, viewId, isCompletedCycle = false } = props; // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; // hooks - const { setToastAlert } = useToast(); - const { issueMap } = useIssues(); const { membership: { currentProjectRole }, } = useUser(); + const { issues, issuesFilter, issueMap } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = + useIssuesActions(storeType); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - const displayFilters = issuesFilterStore.issueFilters?.displayFilters; + const displayFilters = issuesFilter.issueFilters?.displayFilters; - const groupedIssueIds = (issueStore.groupedIssueIds ?? {}) as TGroupedIssues; + const groupedIssueIds = (issues.groupedIssueIds ?? {}) as TGroupedIssues; const onDragEnd = async (result: DropResult) => { if (!result) return; @@ -68,35 +66,25 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { result.destination, workspaceSlug?.toString(), projectId?.toString(), - issueStore, issueMap, groupedIssueIds, - viewId + updateIssue ).catch((err) => { - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: err.detail ?? "Failed to perform this action", }); }); } }; - const handleIssues = useCallback( - async (date: string, issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - await issueActions[action]!(issue); - } - }, - [issueActions] - ); - return ( <>
{ handleIssues(issue.target_date ?? "", issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] - ? async (data) => handleIssues(issue.target_date ?? "", data, EIssueActions.UPDATE) - : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] - ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE) - : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] - ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.ARCHIVE) - : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] - ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.RESTORE) - : undefined + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => + removeIssueFromView && removeIssueFromView(issue.project_id, issue.id) } + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} readOnly={!isEditingAllowed || isCompletedCycle} /> )} - quickAddCallback={issueStore.quickAddIssue} + addIssuesToView={addIssuesToView} + quickAddCallback={issues.quickAddIssue} viewId={viewId} readOnly={!isEditingAllowed || isCompletedCycle} + updateFilters={updateFilters} />
diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index badb849fb..efd785d3e 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -1,17 +1,25 @@ import { observer } from "mobx-react-lite"; // hooks -import { useIssues, useUser } from "hooks/store"; // components -import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // ui import { Spinner } from "@plane/ui"; +import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // types +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TGroupedIssues, + TIssue, + TIssueKanbanFilters, + TIssueMap, +} from "@plane/types"; import { ICalendarWeek } from "./types"; -import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; +import { useIssues, useUser } from "hooks/store"; import { useCalendarView } from "hooks/store/use-calendar-view"; -import { EIssuesStoreType } from "constants/issue"; import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; @@ -30,8 +38,14 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; }; export const CalendarChart: React.FC = observer((props) => { @@ -43,7 +57,9 @@ export const CalendarChart: React.FC = observer((props) => { showWeekends, quickActions, quickAddCallback, + addIssuesToView, viewId, + updateFilters, readOnly = false, } = props; // store hooks @@ -72,7 +88,7 @@ export const CalendarChart: React.FC = observer((props) => { return ( <>
- +
@@ -90,6 +106,7 @@ export const CalendarChart: React.FC = observer((props) => { disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> @@ -106,6 +123,7 @@ export const CalendarChart: React.FC = observer((props) => { disableIssueCreation={!enableIssueCreation || !isEditingAllowed} quickActions={quickActions} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index f92365a58..8ac1e460c 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -1,17 +1,18 @@ import { useState } from "react"; -import { observer } from "mobx-react-lite"; import { Droppable } from "@hello-pangea/dnd"; +import { observer } from "mobx-react-lite"; // components import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // constants import { MONTHS_LIST } from "constants/calendar"; -import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; +// types import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; +import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; type Props = { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; @@ -27,6 +28,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -41,6 +43,7 @@ export const CalendarDayTile: React.FC = observer((props) => { enableQuickIssueCreate, disableIssueCreation, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -112,6 +115,7 @@ export const CalendarDayTile: React.FC = observer((props) => { target_date: renderFormattedPayloadDate(date.date) ?? undefined, }} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} onOpen={() => setShowAllIssues(true)} /> diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx index 2443ae17b..2196c25d8 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; -import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { usePopper } from "react-popper"; +import { Popover, Transition } from "@headlessui/react"; //hooks -import { useCalendarView } from "hooks/store"; // icons import { ChevronLeft, ChevronRight } from "lucide-react"; // constants import { MONTHS_LIST } from "constants/calendar"; +import { useCalendarView } from "hooks/store"; import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index 3168e07ee..3050bba72 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -1,19 +1,25 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; -import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { usePopper } from "react-popper"; +import { Popover, Transition } from "@headlessui/react"; // hooks -import { useCalendarView } from "hooks/store"; // ui -import { ToggleSwitch } from "@plane/ui"; // icons import { Check, ChevronUp } from "lucide-react"; +import { ToggleSwitch } from "@plane/ui"; // types -import { TCalendarLayouts } from "@plane/types"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TCalendarLayouts, + TIssueKanbanFilters, +} from "@plane/types"; // constants import { CALENDAR_LAYOUTS } from "constants/calendar"; import { EIssueFilterType } from "constants/issue"; +import { useCalendarView } from "hooks/store"; import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; @@ -21,14 +27,18 @@ import { IProjectViewIssuesFilter } from "store/issue/project-views"; interface ICalendarHeader { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - viewId?: string; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; } export const CalendarOptionsDropdown: React.FC = observer((props) => { - const { issuesFilterStore, viewId } = props; + const { issuesFilterStore, updateFilters } = props; const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { projectId } = router.query; const issueCalendarView = useCalendarView(); @@ -51,20 +61,14 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false; const handleLayoutChange = (layout: TCalendarLayouts) => { - if (!workspaceSlug || !projectId) return; + if (!projectId || !updateFilters) return; - issuesFilterStore.updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { - calendar: { - ...issuesFilterStore.issueFilters?.displayFilters?.calendar, - layout, - }, + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { + calendar: { + ...issuesFilterStore.issueFilters?.displayFilters?.calendar, + layout, }, - viewId - ); + }); issueCalendarView.updateCalendarPayload( layout === "month" @@ -76,20 +80,14 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const handleToggleWeekends = () => { const showWeekends = issuesFilterStore.issueFilters?.displayFilters?.calendar?.show_weekends ?? false; - if (!workspaceSlug || !projectId) return; + if (!projectId || !updateFilters) return; - issuesFilterStore.updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.DISPLAY_FILTERS, - { - calendar: { - ...issuesFilterStore.issueFilters?.displayFilters?.calendar, - show_weekends: !showWeekends, - }, + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { + calendar: { + ...issuesFilterStore.issueFilters?.displayFilters?.calendar, + show_weekends: !showWeekends, }, - viewId - ); + }); }; return ( diff --git a/web/components/issues/issue-layouts/calendar/header.tsx b/web/components/issues/issue-layouts/calendar/header.tsx index ebbb510fc..aa055534d 100644 --- a/web/components/issues/issue-layouts/calendar/header.tsx +++ b/web/components/issues/issue-layouts/calendar/header.tsx @@ -1,22 +1,33 @@ import { observer } from "mobx-react-lite"; // components +import { ChevronLeft, ChevronRight } from "lucide-react"; import { CalendarMonthsDropdown, CalendarOptionsDropdown } from "components/issues"; // icons -import { ChevronLeft, ChevronRight } from "lucide-react"; import { useCalendarView } from "hooks/store/use-calendar-view"; import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; +import { EIssueFilterType } from "constants/issue"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TIssueKanbanFilters, +} from "@plane/types"; interface ICalendarHeader { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - viewId?: string; + updateFilters?: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; } export const CalendarHeader: React.FC = observer((props) => { - const { issuesFilterStore, viewId } = props; + const { issuesFilterStore, updateFilters } = props; const issueCalendarView = useCalendarView(); @@ -101,7 +112,7 @@ export const CalendarHeader: React.FC = observer((props) => { > Today - +
); diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index b5d0c4346..595cc963c 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -1,16 +1,16 @@ import { useState, useRef } from "react"; -import { observer } from "mobx-react-lite"; import { Draggable } from "@hello-pangea/dnd"; +import { observer } from "mobx-react-lite"; import { MoreHorizontal } from "lucide-react"; // components import { Tooltip, ControlLink } from "@plane/ui"; // hooks +import { cn } from "helpers/common.helper"; +import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers -import { cn } from "helpers/common.helper"; // types import { TIssue, TIssueMap } from "@plane/types"; -import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; type Props = { issues: TIssueMap | undefined; @@ -110,7 +110,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => {
{getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
- +
{issue.name}
diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 6db9323fa..5f62706dc 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -1,20 +1,25 @@ import { useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; +// components +import { ExistingIssuesListModal } from "components/core"; // hooks -import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; +import { useEventTracker, useIssueDetail, useProject } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { createIssuePayload } from "helpers/issue.helper"; // icons import { PlusIcon } from "lucide-react"; +// ui +import { TOAST_TYPE, setPromiseToast, setToast, CustomMenu } from "@plane/ui"; // types -import { TIssue } from "@plane/types"; +import { ISearchIssueResponse, TIssue } from "@plane/types"; // constants import { ISSUE_CREATED } from "constants/event-tracker"; +// helper +import { cn } from "helpers/common.helper"; type Props = { formKey: keyof TIssue; @@ -27,6 +32,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; onOpen?: () => void; }; @@ -59,23 +65,26 @@ const Inputs = (props: any) => { }; export const CalendarQuickAddIssueForm: React.FC = observer((props) => { - const { formKey, prePopulatedData, quickAddCallback, viewId, onOpen } = props; + const { formKey, prePopulatedData, quickAddCallback, addIssuesToView, viewId, onOpen } = props; // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, moduleId } = router.query; // store hooks const { getProjectById } = useProject(); const { captureIssueEvent } = useEventTracker(); + const { updateIssue } = useIssueDetail(); // refs const ref = useRef(null); // states const [isOpen, setIsOpen] = useState(false); - // toast alert - const { setToastAlert } = useToast(); - + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isExistingIssueModalOpen, setIsExistingIssueModalOpen] = useState(false); // derived values const projectDetail = projectId ? getProjectById(projectId.toString()) : null; + const ExistingIssuesListModalPayload = moduleId + ? { module: moduleId.toString(), target_date: "none" } + : { cycle: true, target_date: "none" }; const { reset, @@ -102,13 +111,13 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { Object.keys(errors).forEach((key) => { const error = errors[key as keyof TIssue]; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.message?.toString() || "Some error occurred. Please try again.", }); }); - }, [errors, setToastAlert]); + }, [errors]); const onSubmitHandler = async (formData: TIssue) => { if (isSubmitting || !workspaceSlug || !projectId) return; @@ -120,49 +129,89 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { ...formData, }); - try { - quickAddCallback && - (await quickAddCallback( - workspaceSlug.toString(), - projectId.toString(), - { - ...payload, - }, - viewId - ).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback( + workspaceSlug.toString(), + projectId.toString(), + { + ...payload, + }, + viewId + ); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Calendar quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - console.error(err); - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Calendar quick add" }, - path: router.asPath, - }); - setToastAlert({ - type: "error", + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Calendar quick add" }, + path: router.asPath, + }); + }); + } + }; + + const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !projectId) return; + + const issueIds = data.map((i) => i.id); + + try { + // To handle all updates in parallel + await Promise.all( + data.map((issue) => + updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {}) + ) + ); + if (addIssuesToView) { + await addIssuesToView(issueIds); + } + } catch (error) { + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", - message: err?.message || "Some error occurred. Please try again.", + message: "Something went wrong. Please try again.", }); } }; - const handleOpen = () => { + const handleNewIssue = () => { setIsOpen(true); if (onOpen) onOpen(); }; + const handleExistingIssue = () => { + setIsExistingIssueModalOpen(true); + }; return ( <> + {workspaceSlug && projectId && ( + setIsExistingIssueModalOpen(false)} + searchParams={ExistingIssuesListModalPayload} + handleOnSubmit={handleAddIssuesToView} + /> + )} {isOpen && (
= observer((props) => { )} {!isOpen && ( -
- +
+ {addIssuesToView ? ( + setIsMenuOpen(true)} + onMenuClose={() => setIsMenuOpen(false)} + className="w-full" + customButtonClassName="w-full" + customButton={ +
+ + New Issue +
+ } + > + New Issue + Add existing issue +
+ ) : ( + + )}
)} diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 4daf68b9f..128c84ba5 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -1,59 +1,42 @@ -import { useRouter } from "next/router"; +import { useCallback } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; //hooks import { useCycle, useIssues } from "hooks/store"; // components import { CycleIssueQuickActions } from "components/issues"; -// types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; +// types +// constants import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; export const CycleCalendarLayout: React.FC = observer(() => { - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId || !projectId) return; - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - }), - [issues, workspaceSlug, cycleId, projectId] - ); + const { issues } = useIssues(EIssuesStoreType.CYCLE); if (!cycleId) return null; const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !cycleId) throw new Error(); + return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); + }, + [issues?.addIssueToCycle, workspaceSlug, projectId, cycleId] + ); + return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index cb474d25e..b112b8c3c 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -1,54 +1,37 @@ -import { useRouter } from "next/router"; +import { useCallback } from "react"; import { observer } from "mobx-react-lite"; -// hoks -import { useIssues } from "hooks/store"; +import { useRouter } from "next/router"; +// hooks // components import { ModuleIssueQuickActions } from "components/issues"; -// types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; +// types +// constants import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; +import { useIssues } from "hooks/store"; export const ModuleCalendarLayout: React.FC = observer(() => { - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); const router = useRouter(); - const { workspaceSlug, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; + const { workspaceSlug, projectId, moduleId } = router.query; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, moduleId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - }), - [issues, workspaceSlug, moduleId] + const { issues } = useIssues(EIssuesStoreType.MODULE); + + if (!moduleId) return null; + + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !moduleId) throw new Error(); + return issues.addIssuesToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds); + }, + [issues?.addIssuesToModule, workspaceSlug, projectId, moduleId] ); return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index d42a8c5d2..ad0cffe33 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -1,48 +1,10 @@ import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // hooks -import { useIssues } from "hooks/store"; -// components import { ProjectIssueQuickActions } from "components/issues"; -import { BaseCalendarRoot } from "../base-calendar-root"; -import { EIssueActions } from "../../types"; -import { TIssue } from "@plane/types"; import { EIssuesStoreType } from "constants/issue"; -import { useMemo } from "react"; +// components +import { BaseCalendarRoot } from "../base-calendar-root"; -export const CalendarLayout: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const CalendarLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index 0110aea2b..ff1b654e5 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -1,40 +1,23 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useIssues } from "hooks/store"; -// components import { ProjectIssueQuickActions } from "components/issues"; -import { BaseCalendarRoot } from "../base-calendar-root"; -// types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; -// constants import { EIssuesStoreType } from "constants/issue"; +// components +// types +import { BaseCalendarRoot } from "../base-calendar-root"; +// constants -export interface IViewCalendarLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewCalendarLayout: React.FC = observer((props) => { - const { issueActions } = props; - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); +export const ProjectViewCalendarLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/utils.ts b/web/components/issues/issue-layouts/calendar/utils.ts index 82d9ce0ce..fd96ff647 100644 --- a/web/components/issues/issue-layouts/calendar/utils.ts +++ b/web/components/issues/issue-layouts/calendar/utils.ts @@ -1,21 +1,16 @@ import { DraggableLocation } from "@hello-pangea/dnd"; -import { ICycleIssues } from "store/issue/cycle"; -import { IModuleIssues } from "store/issue/module"; -import { IProjectIssues } from "store/issue/project"; -import { IProjectViewIssues } from "store/issue/project-views"; -import { TGroupedIssues, IIssueMap } from "@plane/types"; +import { TGroupedIssues, IIssueMap, TIssue } from "@plane/types"; export const handleDragDrop = async ( source: DraggableLocation, destination: DraggableLocation, workspaceSlug: string | undefined, projectId: string | undefined, - store: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues, issueMap: IIssueMap, issueWithIds: TGroupedIssues, - viewId: string | null = null // it can be moduleId, cycleId + updateIssue?: (projectId: string, issueId: string, data: Partial) => Promise ) => { - if (!issueMap || !issueWithIds || !workspaceSlug || !projectId) return; + if (!issueMap || !issueWithIds || !workspaceSlug || !projectId || !updateIssue) return; const sourceColumnId = source?.droppableId || null; const destinationColumnId = destination?.droppableId || null; @@ -31,12 +26,11 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); const removedIssueDetail = issueMap[removed]; - const updateIssue = { + const updatedIssue = { id: removedIssueDetail?.id, target_date: destinationColumnId, }; - if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue, viewId); - else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); + return await updateIssue(projectId, updatedIssue.id, updatedIssue); } }; diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 5a640a566..ec1d12e59 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -4,12 +4,12 @@ import { CalendarDayTile } from "components/issues"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { ICalendarDate, ICalendarWeek } from "./types"; -import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; import { ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssuesFilter } from "store/issue/project-views"; +import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; +import { ICalendarDate, ICalendarWeek } from "./types"; type Props = { issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; @@ -25,6 +25,7 @@ type Props = { data: TIssue, viewId?: string ) => Promise; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; readOnly?: boolean; }; @@ -39,6 +40,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { enableQuickIssueCreate, disableIssueCreation, quickAddCallback, + addIssuesToView, viewId, readOnly = false, } = props; @@ -68,6 +70,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { enableQuickIssueCreate={enableQuickIssueCreate} disableIssueCreation={disableIssueCreation} quickAddCallback={quickAddCallback} + addIssuesToView={addIssuesToView} viewId={viewId} readOnly={readOnly} /> diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index 0c8fb377a..c9de2279c 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -1,49 +1,27 @@ +import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import size from "lodash/size"; -import { useTheme } from "next-themes"; // hooks -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // constants -import { EUserProjectRoles } from "constants/project"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ProjectArchivedEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -61,33 +39,20 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["archived"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["archived"].secondaryButton?.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["archived"].title, - description: EMPTY_ISSUE_STATE_DETAILS["archived"].description, - image: EmptyStateImagePath, - primaryButton: { - text: EMPTY_ISSUE_STATE_DETAILS["archived"].primaryButton.text, - onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`), - }, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = + issueFilterCount > 0 ? EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER : EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; return (
- + 0 ? undefined : `/${workspaceSlug}/projects/${projectId}/settings/automations` + } + secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 4b7676173..350e4dbb4 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,19 +1,17 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { PlusIcon } from "lucide-react"; -import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components +import { useApplication, useEventTracker, useIssues } from "hooks/store"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +// components +import { EmptyState } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; -import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -24,88 +22,43 @@ type Props = { isEmptyFilters?: boolean; }; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const CycleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.CYCLE); - const { updateIssue, fetchIssue } = useIssueDetail(); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole: userRole }, - currentUser, - } = useUser(); - - const { setToastAlert } = useToast(); const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !cycleId) return; const issueIds = data.map((i) => i.id); - await issues.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); - }); + await issues + .addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds) + .then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }) + ) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Selected issues could not be added to the cycle. Please try again.", + }) + ); }; - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode); - - const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = isEmptyFilters - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: emptyStateDetail.title, - description: emptyStateDetail.description, - image: emptyStateImage, - primaryButton: { - text: emptyStateDetail.primaryButton.text, - icon: , - onClick: () => { - setTrackElement("Cycle issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); - }, - }, - secondaryButton: { - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setCycleIssuesListModal(true), - }, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; + const additionalPath = activeLayout ?? "list"; + const emptyStateSize = isEmptyFilters ? "lg" : "sm"; return ( <> @@ -118,7 +71,20 @@ export const CycleEmptyState: React.FC = observer((props) => { handleOnSubmit={handleAddIssuesToCycle} />
- + { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + } + } + secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true)} + />
); diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index c496cc5fe..0968ed07a 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -1,49 +1,26 @@ +import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import size from "lodash/size"; -import { useTheme } from "next-themes"; // hooks -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // constants -import { EUserProjectRoles } from "constants/project"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ProjectDraftEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("draft", "draft-issues-empty", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -61,29 +38,19 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["draft"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["draft"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["draft"].title, - description: EMPTY_ISSUE_STATE_DETAILS["draft"].description, - image: EmptyStateImagePath, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = + issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; + const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm"; return (
- + 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/empty-states/global-view.tsx b/web/components/issues/issue-layouts/empty-states/global-view.tsx index bf898aec4..b24c4d5d6 100644 --- a/web/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/global-view.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react-lite"; import { Plus, PlusIcon } from "lucide-react"; // hooks +import { EmptyState } from "components/common"; import { useApplication, useEventTracker, useProject } from "hooks/store"; // components -import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; import emptyProject from "public/empty-state/project.svg"; diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index ef7ec729c..6c0cd0cd6 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -1,19 +1,18 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { PlusIcon } from "lucide-react"; -import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +import { useApplication, useEventTracker, useIssues } from "hooks/store"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; +// ui // components import { ExistingIssuesListModal } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -24,35 +23,16 @@ type Props = { isEmptyFilters?: boolean; }; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ModuleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.MODULE); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole: userRole }, - currentUser, - } = useUser(); - // toast alert - const { setToastAlert } = useToast(); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !moduleId) return; @@ -60,51 +40,24 @@ export const ModuleEmptyState: React.FC = observer((props) => { const issueIds = data.map((i) => i.id); await issues .addIssuesToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds) + .then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the module successfully.", + }) + ) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the module. Please try again.", }) ); }; - const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const emptyStateImage = getEmptyStateImagePath("module-issues", activeLayout ?? "list", isLightMode); - - const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = isEmptyFilters - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: emptyStateDetail.title, - description: emptyStateDetail.description, - image: emptyStateImage, - primaryButton: { - text: emptyStateDetail.primaryButton.text, - icon: , - onClick: () => { - setTrackElement("Module issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.MODULE); - }, - }, - secondaryButton: { - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setModuleIssuesListModal(true), - }, - disabled: !isEditingAllowed, - }; + const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES; + const additionalPath = activeLayout ?? "list"; return ( <> @@ -117,7 +70,19 @@ export const ModuleEmptyState: React.FC = observer((props) => { handleOnSubmit={handleAddIssuesToModule} />
- + { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + } + } + secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)} + />
); diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index c7185934c..12642d364 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -1,51 +1,29 @@ +import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import size from "lodash/size"; -import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useIssues } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // constants -import { EUserProjectRoles } from "constants/project"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; // types import { IIssueFilterOptions } from "@plane/types"; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ProjectEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); + const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -63,40 +41,26 @@ export const ProjectEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["project"].title, - description: EMPTY_ISSUE_STATE_DETAILS["project"].description, - image: EmptyStateImagePath, - comicBox: { - title: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.title, - description: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.description, - }, - primaryButton: { - text: EMPTY_ISSUE_STATE_DETAILS["project"].primaryButton.text, - onClick: () => { - setTrackElement("Project issue empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, - }, - size: "lg", - disabled: !isEditingAllowed, - }; + const emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; + const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm"; return (
- + 0 + ? undefined + : () => { + setTrackElement("Project issue empty state"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); + } + } + secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/empty-states/project-view.tsx b/web/components/issues/issue-layouts/empty-states/project-view.tsx index 2da9a826f..fd98011fa 100644 --- a/web/components/issues/issue-layouts/empty-states/project-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-view.tsx @@ -1,12 +1,12 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { EmptyState } from "components/common"; +import { EIssuesStoreType } from "constants/issue"; import { useApplication, useEventTracker } from "hooks/store"; // components -import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; -import { EIssuesStoreType } from "constants/issue"; export const ProjectViewEmptyState: React.FC = observer(() => { // store hooks diff --git a/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx new file mode 100644 index 000000000..76f36e815 --- /dev/null +++ b/web/components/issues/issue-layouts/filters/applied-filters/cycle.tsx @@ -0,0 +1,48 @@ +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +// hooks +import { CycleGroupIcon } from "@plane/ui"; +import { useCycle } from "hooks/store"; +// ui +// types +import { TCycleGroups } from "@plane/types"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; + editable: boolean | undefined; +}; + +export const AppliedCycleFilters: React.FC = observer((props) => { + const { handleRemove, values, editable } = props; + // store hooks + const { getCycleById } = useCycle(); + + return ( + <> + {values.map((cycleId) => { + const cycleDetails = getCycleById(cycleId) ?? null; + + if (!cycleDetails) return null; + + const cycleStatus = (cycleDetails?.status ? cycleDetails?.status.toLocaleLowerCase() : "draft") as TCycleGroups; + + return ( +
+ + {cycleDetails.name} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/date.tsx b/web/components/issues/issue-layouts/filters/applied-filters/date.tsx index 891fd6ddd..fdaed4b9b 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/date.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/date.tsx @@ -2,10 +2,10 @@ import { observer } from "mobx-react-lite"; // icons import { X } from "lucide-react"; // helpers +import { DATE_FILTER_OPTIONS } from "constants/filters"; import { renderFormattedDate } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; // constants -import { DATE_FILTER_OPTIONS } from "constants/filters"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 4ca2538e5..10ad265f3 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -1,23 +1,25 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks -import { useUser } from "hooks/store"; -// components import { + AppliedCycleFilters, AppliedDateFilters, AppliedLabelsFilters, AppliedMembersFilters, + AppliedModuleFilters, AppliedPriorityFilters, AppliedProjectFilters, AppliedStateFilters, AppliedStateGroupFilters, } from "components/issues"; -// helpers +import { EUserProjectRoles } from "constants/project"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +import { useApplication, useUser } from "hooks/store"; +// components +// helpers // types import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; type Props = { appliedFilters: IIssueFilterOptions; @@ -34,6 +36,9 @@ const dateFilters = ["start_date", "target_date"]; export const AppliedFiltersList: React.FC = observer((props) => { const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states, alwaysAllowEditing } = props; // store hooks + const { + router: { moduleId, cycleId }, + } = useApplication(); const { membership: { currentProjectRole }, } = useUser(); @@ -104,6 +109,20 @@ export const AppliedFiltersList: React.FC = observer((props) => { values={value} /> )} + {filterKey === "cycle" && !cycleId && ( + handleRemoveFilter("cycle", val)} + values={value} + /> + )} + {filterKey === "module" && !moduleId && ( + handleRemoveFilter("module", val)} + values={value} + /> + )} {isEditingAllowed && ( + )} +
+ ); + })} + + ); +}); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx index be3240b55..aad394d8a 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react-lite"; // icons -import { PriorityIcon } from "@plane/ui"; import { X } from "lucide-react"; +import { PriorityIcon } from "@plane/ui"; // types import { TIssuePriorities } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx index 4c5affe8d..84e81b6e8 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -2,8 +2,8 @@ import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // hooks import { useProject } from "hooks/store"; -// helpers -import { renderEmoji } from "helpers/emoji.helper"; +// components +import { ProjectLogo } from "components/project"; type Props = { handleRemove: (val: string) => void; @@ -25,15 +25,9 @@ export const AppliedProjectFilters: React.FC = observer((props) => { return (
- {projectDetails.emoji ? ( - {renderEmoji(projectDetails.emoji)} - ) : projectDetails.icon_prop ? ( -
{renderEmoji(projectDetails.icon_prop)}
- ) : ( - - {projectDetails?.name.charAt(0)} - - )} + + + {projectDetails.name} {editable && ( - ) - )} + {ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => ( + + ))}
)} diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx index 0feb1d891..6de3c940d 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react-lite"; // components import { FilterOption } from "components/issues"; // types +import { ISSUE_EXTRA_OPTIONS } from "constants/issue"; import { IIssueDisplayFilterOptions, TIssueExtraOptions } from "@plane/types"; // constants -import { ISSUE_EXTRA_OPTIONS } from "constants/issue"; type Props = { selectedExtraOptions: { diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index 659d86d08..10dfa8c7c 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -1,21 +1,21 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; - // components import { FilterHeader, FilterOption } from "components/issues"; // types +import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types"; // constants -import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; type Props = { displayFilters: IIssueDisplayFilterOptions; groupByOptions: TIssueGroupByOptions[]; handleUpdate: (val: TIssueGroupByOptions) => void; + ignoreGroupedFilters: Partial[]; }; export const FilterGroupBy: React.FC = observer((props) => { - const { displayFilters, groupByOptions, handleUpdate } = props; + const { displayFilters, groupByOptions, handleUpdate, ignoreGroupedFilters } = props; const [previewEnabled, setPreviewEnabled] = useState(true); @@ -34,6 +34,7 @@ export const FilterGroupBy: React.FC = observer((props) => { {ISSUE_GROUP_BY_OPTIONS.filter((option) => groupByOptions.includes(option.key)).map((groupBy) => { if (displayFilters.layout === "kanban" && selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy) return null; + if (ignoreGroupedFilters.includes(groupBy?.key)) return null; return ( void; subGroupByOptions: TIssueGroupByOptions[]; + ignoreGroupedFilters: Partial[]; }; export const FilterSubGroupBy: React.FC = observer((props) => { - const { displayFilters, handleUpdate, subGroupByOptions } = props; + const { displayFilters, handleUpdate, subGroupByOptions, ignoreGroupedFilters } = props; const [previewEnabled, setPreviewEnabled] = useState(true); @@ -33,6 +33,7 @@ export const FilterSubGroupBy: React.FC = observer((props) => {
{ISSUE_GROUP_BY_OPTIONS.filter((option) => subGroupByOptions.includes(option.key)).map((subGroupBy) => { if (selectedGroupBy !== null && subGroupBy.key === selectedGroupBy) return null; + if (ignoreGroupedFilters.includes(subGroupBy?.key)) return null; return ( = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter((memberId) => - getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter( + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { diff --git a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx index 7bde26ab9..45e3309a9 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Avatar, Loader } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components -import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Avatar, Loader } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -22,8 +22,8 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { // store hooks const { getUserDetails } = useMember(); - const filteredOptions = memberIds?.filter((memberId) => - getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter( + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const appliedFiltersCount = appliedFilters?.length ?? 0; diff --git a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx new file mode 100644 index 000000000..396addde6 --- /dev/null +++ b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx @@ -0,0 +1,96 @@ +import React, { useState } from "react"; +import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; +// components +import { Loader, CycleGroupIcon } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; +import { useApplication, useCycle } from "hooks/store"; +// ui +// types +import { TCycleGroups } from "@plane/types"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterCycle: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + // hooks + const { + router: { projectId }, + } = useApplication(); + const { getCycleById, getProjectCycleIds } = useCycle(); + + // states + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + + const cycleIds = projectId ? getProjectCycleIds(projectId) : undefined; + const cycles = cycleIds?.map((projectId) => getCycleById(projectId)!) ?? null; + const appliedFiltersCount = appliedFilters?.length ?? 0; + const filteredOptions = sortBy( + cycles?.filter((cycle) => cycle.name.toLowerCase().includes(searchQuery.toLowerCase())), + (cycle) => cycle.name.toLowerCase() + ); + + const handleViewToggle = () => { + if (!filteredOptions) return; + + if (itemsToRender === filteredOptions.length) setItemsToRender(5); + else setItemsToRender(filteredOptions.length); + }; + + const cycleStatus = (status: TCycleGroups) => (status ? status.toLocaleLowerCase() : "draft") as TCycleGroups; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + handleUpdate(cycle.id)} + icon={ + + } + title={cycle.name} + activePulse={cycleStatus(cycle?.status) === "current" ? true : false} + /> + ))} + {filteredOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index af8cfc84a..257aa1977 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search, X } from "lucide-react"; -// components +// hooks import { FilterAssignees, FilterMentions, @@ -13,11 +13,15 @@ import { FilterState, FilterStateGroup, FilterTargetDate, + FilterCycle, + FilterModule, } from "components/issues"; +import { ILayoutDisplayFiltersOptions } from "constants/issue"; +import { useApplication } from "hooks/store"; +// components // types import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // constants -import { ILayoutDisplayFiltersOptions } from "constants/issue"; type Props = { filters: IIssueFilterOptions; @@ -30,6 +34,10 @@ type Props = { export const FilterSelection: React.FC = observer((props) => { const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props; + // hooks + const { + router: { moduleId, cycleId }, + } = useApplication(); // states const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); @@ -55,7 +63,7 @@ export const FilterSelection: React.FC = observer((props) => { )}
-
+
{/* priority */} {isFilterEnabled("priority") && (
@@ -102,6 +110,28 @@ export const FilterSelection: React.FC = observer((props) => {
)} + {/* cycle */} + {isFilterEnabled("cycle") && !cycleId && ( +
+ handleFiltersUpdate("cycle", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* module */} + {isFilterEnabled("module") && !moduleId && ( +
+ handleFiltersUpdate("module", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + {/* assignees */} {isFilterEnabled("mentions") && (
diff --git a/web/components/issues/issue-layouts/filters/header/filters/index.ts b/web/components/issues/issue-layouts/filters/header/filters/index.ts index 2d3a04d0f..ab5756bf4 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/index.ts +++ b/web/components/issues/issue-layouts/filters/header/filters/index.ts @@ -8,4 +8,6 @@ export * from "./project"; export * from "./start-date"; export * from "./state-group"; export * from "./state"; +export * from "./cycle"; +export * from "./module"; export * from "./target-date"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx index b226f42b3..42e955535 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // components +import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Loader } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx index a6af9833a..4d2839b2c 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Loader, Avatar } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components -import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Loader, Avatar } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -24,8 +24,8 @@ export const FilterMentions: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter((memberId) => - getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter( + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { diff --git a/web/components/issues/issue-layouts/filters/header/filters/module.tsx b/web/components/issues/issue-layouts/filters/header/filters/module.tsx new file mode 100644 index 000000000..812cf939f --- /dev/null +++ b/web/components/issues/issue-layouts/filters/header/filters/module.tsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; +// components +import { Loader, DiceIcon } from "@plane/ui"; +import { FilterHeader, FilterOption } from "components/issues"; +import { useApplication, useModule } from "hooks/store"; +// ui + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterModule: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + // hooks + const { + router: { projectId }, + } = useApplication(); + const { getModuleById, getProjectModuleIds } = useModule(); + + // states + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + + const moduleIds = projectId ? getProjectModuleIds(projectId) : undefined; + const modules = moduleIds?.map((projectId) => getModuleById(projectId)!) ?? null; + const appliedFiltersCount = appliedFilters?.length ?? 0; + const filteredOptions = sortBy( + modules?.filter((module) => module.name.toLowerCase().includes(searchQuery.toLowerCase())), + (module) => module.name.toLowerCase() + ); + + const handleViewToggle = () => { + if (!filteredOptions) return; + + if (itemsToRender === filteredOptions.length) setItemsToRender(5); + else setItemsToRender(filteredOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + handleUpdate(cycle.id)} + icon={} + title={cycle.name} + /> + ))} + {filteredOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/filters/header/filters/project.tsx b/web/components/issues/issue-layouts/filters/header/filters/project.tsx index 61b7d50c1..b9f864b4b 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/project.tsx @@ -1,13 +1,14 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // components +import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // hooks import { useProject } from "hooks/store"; +// components +import { ProjectLogo } from "components/project"; // ui -import { Loader } from "@plane/ui"; // helpers -import { renderEmoji } from "helpers/emoji.helper"; type Props = { appliedFilters: string[] | null; @@ -52,19 +53,9 @@ export const FilterProjects: React.FC = observer((props) => { isChecked={appliedFilters?.includes(project.id) ? true : false} onClick={() => handleUpdate(project.id)} icon={ - project.emoji ? ( - - {renderEmoji(project.emoji)} - - ) : project.icon_prop ? ( -
- {renderEmoji(project.icon_prop)} -
- ) : ( - - {project?.name.charAt(0)} - - ) + + + } title={project.name} /> diff --git a/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx b/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx index 2cb715158..87def7e29 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components -import { FilterHeader, FilterOption } from "components/issues"; import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; // constants import { DATE_FILTER_OPTIONS } from "constants/filters"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx index ea9097146..06c1aae9f 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx @@ -2,9 +2,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components +import { StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // icons -import { StateGroupIcon } from "@plane/ui"; import { STATE_GROUPS } from "constants/state"; // constants diff --git a/web/components/issues/issue-layouts/filters/header/filters/state.tsx b/web/components/issues/issue-layouts/filters/header/filters/state.tsx index c13a69b0a..5dde1d279 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // components +import { Loader, StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; // ui -import { Loader, StateGroupIcon } from "@plane/ui"; // types import { IState } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx b/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx index b168af668..9e0ce18a7 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // components -import { FilterHeader, FilterOption } from "components/issues"; import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; // constants import { DATE_FILTER_OPTIONS } from "constants/filters"; diff --git a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx index 33b86ada1..0d00c3675 100644 --- a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx +++ b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx @@ -1,11 +1,11 @@ import React, { Fragment, useState } from "react"; +import { Placement } from "@popperjs/core"; import { usePopper } from "react-popper"; import { Popover, Transition } from "@headlessui/react"; -import { Placement } from "@popperjs/core"; // ui +import { ChevronUp } from "lucide-react"; import { Button } from "@plane/ui"; // icons -import { ChevronUp } from "lucide-react"; type Props = { children: React.ReactNode; @@ -34,22 +34,26 @@ export const FiltersDropdown: React.FC = (props) => { return ( <> - {menuButton ? : } + {menuButton ? ( + + ) : ( + + )} void; multiple?: boolean; + activePulse?: boolean; }; export const FilterOption: React.FC = (props) => { - const { icon, isChecked, multiple = true, onClick, title } = props; + const { icon, isChecked, multiple = true, onClick, title, activePulse = false } = props; return (
+ {activePulse && ( +
+ )} ); }; diff --git a/web/components/issues/issue-layouts/filters/header/layout-selection.tsx b/web/components/issues/issue-layouts/filters/header/layout-selection.tsx index 305be7eff..a69ead577 100644 --- a/web/components/issues/issue-layouts/filters/header/layout-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/layout-selection.tsx @@ -3,9 +3,9 @@ import React from "react"; // ui import { Tooltip } from "@plane/ui"; // types +import { ISSUE_LAYOUTS } from "constants/issue"; import { TIssueLayouts } from "@plane/types"; // constants -import { ISSUE_LAYOUTS } from "constants/issue"; type Props = { layouts: TIssueLayouts[]; diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index ec33872eb..11f52db80 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -1,50 +1,49 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useIssues, useUser } from "hooks/store"; -// components -import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues"; import { GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "components/gantt-chart"; -// helpers +import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues"; +import { EUserProjectRoles } from "constants/project"; import { renderIssueBlocksStructure } from "helpers/issue.helper"; +import { useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; +// components +// helpers // types import { TIssue, TUnGroupedIssues } from "@plane/types"; -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssueActions } from "../types"; +import { EIssuesStoreType } from "constants/issue"; +type GanttStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW; interface IBaseGanttRoot { - issueFiltersStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues; viewId?: string; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - }; + storeType: GanttStoreType; } export const BaseGanttRoot: React.FC = observer((props: IBaseGanttRoot) => { - const { issueFiltersStore, issueStore, viewId } = props; + const { viewId, storeType } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; + + const { issues, issuesFilter } = useIssues(storeType); + const { updateIssue } = useIssuesActions(storeType); // store hooks const { membership: { currentProjectRole }, } = useUser(); const { issueMap } = useIssues(); - const appliedDisplayFilters = issueFiltersStore.issueFilters?.displayFilters; + const appliedDisplayFilters = issuesFilter.issueFilters?.displayFilters; - const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues; - const { enableIssueCreation } = issueStore?.viewFlags || {}; + const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues; + const { enableIssueCreation } = issues?.viewFlags || {}; - const issues = issueIds.map((id) => issueMap?.[id]); + const issuesArray = issueIds.map((id) => issueMap?.[id]); const updateIssueBlockStructure = async (issue: TIssue, data: IBlockUpdateData) => { if (!workspaceSlug) return; @@ -52,7 +51,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan const payload: any = { ...data }; if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder; - await issueStore.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, payload, viewId); + updateIssue && (await updateIssue(issue.project_id, issue.id, payload)); }; const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -64,7 +63,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan border={false} title="Issues" loaderTitle="Issues" - blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null} + blocks={issues ? renderIssueBlocksStructure(issuesArray) : null} blockUpdateHandler={updateIssueBlockStructure} blockToRender={(data: TIssue) => } sidebarToRender={(props) => } @@ -75,7 +74,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan enableAddBlock={isAllowed} quickAdd={ enableIssueCreation && isAllowed ? ( - + ) : undefined } showAllBlocks diff --git a/web/components/issues/issue-layouts/gantt/blocks.tsx b/web/components/issues/issue-layouts/gantt/blocks.tsx index 209d876ac..98b05d0ca 100644 --- a/web/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/components/issues/issue-layouts/gantt/blocks.tsx @@ -1,10 +1,10 @@ import { observer } from "mobx-react"; // hooks -import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; // ui import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; +import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store"; type Props = { issueId: string; @@ -97,7 +97,7 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => {
{projectIdentifier} {issueDetails?.sequence_id}
- + {issueDetails?.name}
diff --git a/web/components/issues/issue-layouts/gantt/cycle-root.tsx b/web/components/issues/issue-layouts/gantt/cycle-root.tsx index cf1f6121a..923845e7b 100644 --- a/web/components/issues/issue-layouts/gantt/cycle-root.tsx +++ b/web/components/issues/issue-layouts/gantt/cycle-root.tsx @@ -1,48 +1,14 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks -import { useCycle, useIssues } from "hooks/store"; +import { EIssuesStoreType } from "constants/issue"; // components import { BaseGanttRoot } from "./base-gantt-root"; -import { EIssuesStoreType } from "constants/issue"; -import { EIssueActions } from "../types"; -import { TIssue } from "@plane/types"; export const CycleGanttLayout: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, cycleId } = router.query; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); - const { fetchCycleDetails } = useCycle(); + const { cycleId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId || !issue.id) return; - - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString()); - }, - }; - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/gantt/module-root.tsx b/web/components/issues/issue-layouts/gantt/module-root.tsx index c7c8e8b03..e14f1339a 100644 --- a/web/components/issues/issue-layouts/gantt/module-root.tsx +++ b/web/components/issues/issue-layouts/gantt/module-root.tsx @@ -1,48 +1,14 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks -import { useIssues, useModule } from "hooks/store"; +import { EIssuesStoreType } from "constants/issue"; // components import { BaseGanttRoot } from "./base-gantt-root"; -import { EIssuesStoreType } from "constants/issue"; -import { EIssueActions } from "../types"; -import { TIssue } from "@plane/types"; export const ModuleGanttLayout: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug, moduleId } = router.query; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - const { fetchModuleDetails } = useModule(); + const { moduleId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); - fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId || !issue.id) return; - - await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); - fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString()); - }, - }; - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/gantt/project-root.tsx b/web/components/issues/issue-layouts/gantt/project-root.tsx index 18fd3ecef..d8a2cd1a1 100644 --- a/web/components/issues/issue-layouts/gantt/project-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-root.tsx @@ -1,33 +1,8 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useIssues } from "hooks/store"; +import { EIssuesStoreType } from "constants/issue"; // components import { BaseGanttRoot } from "./base-gantt-root"; -import { EIssuesStoreType } from "constants/issue"; -import { EIssueActions } from "../types"; -import { TIssue } from "@plane/types"; -export const GanttLayout: React.FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug } = router.query; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = { - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }; - - return ; -}); +export const GanttLayout: React.FC = observer(() => ); diff --git a/web/components/issues/issue-layouts/gantt/project-view-root.tsx b/web/components/issues/issue-layouts/gantt/project-view-root.tsx index 1ed02c2c9..80d5e047b 100644 --- a/web/components/issues/issue-layouts/gantt/project-view-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-view-root.tsx @@ -1,37 +1,16 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useIssues } from "hooks/store"; +import { EIssuesStoreType } from "constants/issue"; // components import { BaseGanttRoot } from "./base-gantt-root"; // constants -import { EIssuesStoreType } from "constants/issue"; // types -import { EIssueActions } from "../types"; -import { TIssue } from "@plane/types"; -export interface IViewGanttLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewGanttLayout: React.FC = observer((props) => { - const { issueActions } = props; - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); +export const ProjectViewGanttLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index 7ed6a8730..b2d3ac9d4 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -1,21 +1,22 @@ import { useEffect, useState, useRef, FC } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { setPromiseToast } from "@plane/ui"; +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; -import { createIssuePayload } from "helpers/issue.helper"; -import { cn } from "helpers/common.helper"; +// ui // types import { IProject, TIssue } from "@plane/types"; -// constants import { ISSUE_CREATED } from "constants/event-tracker"; +// constants interface IInputProps { formKey: string; @@ -70,7 +71,6 @@ export const GanttQuickAddIssueForm: React.FC = observe // hooks const { getProjectById } = useProject(); const { captureIssueEvent } = useEventTracker(); - const { setToastAlert } = useToast(); const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined; @@ -110,31 +110,35 @@ export const GanttQuickAddIssueForm: React.FC = observe target_date: renderFormattedPayloadDate(targetDate), }); - try { - quickAddCallback && - (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Gantt quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Gantt quick add" }, - path: router.asPath, - }); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Gantt quick add" }, + path: router.asPath, + }); + }); } }; return ( @@ -159,7 +163,7 @@ export const GanttQuickAddIssueForm: React.FC = observe ) : (
diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 8446e7328..dabecc491 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -2,18 +2,17 @@ import { MutableRefObject, memo } from "react"; import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; // hooks +import { Tooltip, ControlLink } from "@plane/ui"; +import RenderIfVisible from "components/core/render-if-visible-HOC"; +import { cn } from "helpers/common.helper"; import { useApplication, useIssueDetail, useProject } from "hooks/store"; // components -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; -import { IssueProperties } from "../properties/all-properties"; -// ui -import { Tooltip, ControlLink } from "@plane/ui"; -// types import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; -import { EIssueActions } from "../types"; +import { IssueProperties } from "../properties/all-properties"; +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +// ui +// types // helper -import { cn } from "helpers/common.helper"; -import RenderIfVisible from "components/core/render-if-visible-HOC"; interface IssueBlockProps { peekIssueId?: string; @@ -23,7 +22,7 @@ interface IssueBlockProps { isDragDisabled: boolean; draggableId: string; index: number; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -34,13 +33,13 @@ interface IssueBlockProps { interface IssueDetailsBlockProps { issue: TIssue; displayProperties: IIssueDisplayProperties | undefined; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; isReadOnly: boolean; } const KanbanIssueDetailsBlock: React.FC = observer((props: IssueDetailsBlockProps) => { - const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props; + const { issue, updateIssue, quickActions, isReadOnly, displayProperties } = props; // hooks const { getProjectIdentifierById } = useProject(); const { @@ -48,10 +47,6 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop } = useApplication(); const { setPeekIssue } = useIssueDetail(); - const updateIssue = async (issueToUpdate: TIssue) => { - if (issueToUpdate) await handleIssues(issueToUpdate, EIssueActions.UPDATE); - }; - const handleIssuePeekOverview = (issue: TIssue) => workspaceSlug && issue && @@ -71,7 +66,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop {issue?.is_draft ? ( - + {issue.name} ) : ( @@ -84,7 +79,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" disabled={!!issue?.tempId} > - + {issue.name} @@ -95,7 +90,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop issue={issue} displayProperties={displayProperties} activeLayout="Kanban" - handleIssues={updateIssue} + updateIssue={updateIssue} isReadOnly={isReadOnly} /> @@ -111,7 +106,7 @@ export const KanbanIssueBlock: React.FC = memo((props) => { isDragDisabled, draggableId, index, - handleIssues, + updateIssue, quickActions, canEditProperties, scrollableContainerRef, @@ -141,7 +136,7 @@ export const KanbanIssueBlock: React.FC = memo((props) => { >
= memo((props) => { classNames="space-y-2 px-3 py-2" root={scrollableContainerRef} defaultHeight="100px" - horizonatlOffset={50} + horizontalOffset={50} alwaysRender={snapshot.isDragging} pauseHeightUpdateWhileRendering={isDragStarted} changingReference={issueIds} @@ -159,7 +154,7 @@ export const KanbanIssueBlock: React.FC = memo((props) => { diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index 3746111e5..7a58a4933 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -1,9 +1,8 @@ import { MutableRefObject, memo } from "react"; //types -import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; -import { EIssueActions } from "../types"; -// components import { KanbanIssueBlock } from "components/issues"; +import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; +// components interface IssueBlocksListProps { sub_group_id: string; @@ -13,7 +12,7 @@ interface IssueBlocksListProps { issueIds: string[]; displayProperties: IIssueDisplayProperties | undefined; isDragDisabled: boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -29,7 +28,7 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { issueIds, displayProperties, isDragDisabled, - handleIssues, + updateIssue, quickActions, canEditProperties, scrollableContainerRef, @@ -54,7 +53,7 @@ const KanbanIssueBlocksListMemo: React.FC = (props) => { issueId={issueId} issuesMap={issuesMap} displayProperties={displayProperties} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} draggableId={draggableId} index={index} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index b645afe30..394f5ef18 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -1,9 +1,17 @@ +import { MutableRefObject } from "react"; import { observer } from "mobx-react-lite"; +// constants // hooks -import { useIssueDetail, useKanbanView, useLabel, useMember, useProject, useProjectState } from "hooks/store"; -// components -import { HeaderGroupByCard } from "./headers/group-by-card"; -import { KanbanGroup } from "./kanban-group"; +import { + useCycle, + useIssueDetail, + useKanbanView, + useLabel, + useMember, + useModule, + useProject, + useProjectState, +} from "hooks/store"; // types import { GroupByColumnTypes, @@ -16,11 +24,12 @@ import { TUnGroupedIssues, TIssueKanbanFilters, } from "@plane/types"; -// constants -import { EIssueActions } from "../types"; +// parent components import { getGroupByColumns } from "../utils"; -import { TCreateModalStoreTypes } from "constants/issue"; -import { MutableRefObject } from "react"; +// components +import { HeaderGroupByCard } from "./headers/group-by-card"; +import { KanbanGroup } from "./kanban-group"; +import { KanbanStoreType } from "./base-kanban-root"; export interface IGroupByKanBan { issuesMap: IIssueMap; @@ -30,7 +39,7 @@ export interface IGroupByKanBan { group_by: string | null; sub_group_id: string; isDragDisabled: boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: any; @@ -43,7 +52,7 @@ export interface IGroupByKanBan { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -60,7 +69,7 @@ const GroupByKanBan: React.FC = observer((props) => { group_by, sub_group_id = "null", isDragDisabled, - handleIssues, + updateIssue, quickActions, kanbanFilters, handleKanbanFilters, @@ -79,33 +88,53 @@ const GroupByKanBan: React.FC = observer((props) => { const member = useMember(); const project = useProject(); const label = useLabel(); + const cycle = useCycle(); + const moduleInfo = useModule(); const projectState = useProjectState(); const { peekIssue } = useIssueDetail(); - const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member); + const list = getGroupByColumns( + group_by as GroupByColumnTypes, + project, + cycle, + moduleInfo, + label, + projectState, + member + ); if (!list) return null; - const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)[_list.id]?.length > 0); + const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0); const groupList = showEmptyGroup ? list : groupWithIssues; - const visibilityGroupBy = (_list: IGroupByColumn) => - sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false; + const visibilityGroupBy = (_list: IGroupByColumn) => { + if (sub_group_by) { + if (kanbanFilters?.sub_group_by.includes(_list.id)) return true; + return false; + } else { + if (kanbanFilters?.group_by.includes(_list.id)) return true; + return false; + } + }; const isGroupByCreatedBy = group_by === "created_by"; return ( -
+
{groupList && groupList.length > 0 && groupList.map((_list: IGroupByColumn) => { const groupByVisibilityToggle = visibilityGroupBy(_list); return ( -
+
{sub_group_by === null && ( -
+
= observer((props) => { group_by={group_by} sub_group_id={sub_group_id} isDragDisabled={isDragDisabled} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} enableQuickIssueCreate={enableQuickIssueCreate} quickAddCallback={quickAddCallback} @@ -160,7 +189,7 @@ export interface IKanBan { sub_group_by: string | null; group_by: string | null; sub_group_id?: string; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; @@ -174,7 +203,7 @@ export interface IKanBan { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; @@ -189,7 +218,7 @@ export const KanBan: React.FC = observer((props) => { sub_group_by, group_by, sub_group_id = "null", - handleIssues, + updateIssue, quickActions, kanbanFilters, handleKanbanFilters, @@ -216,7 +245,7 @@ export const KanBan: React.FC = observer((props) => { sub_group_by={sub_group_by} sub_group_id={sub_group_id} isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index 440b379b8..a14dd5ddc 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -1,19 +1,19 @@ import React, { FC } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -// components -import { CustomMenu } from "@plane/ui"; -import { ExistingIssuesListModal } from "components/core"; -import { CreateUpdateIssueModal } from "components/issues"; // lucide icons import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ExistingIssuesListModal } from "components/core"; +import { CreateUpdateIssueModal } from "components/issues"; +// constants // hooks -import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; -// mobx -import { observer } from "mobx-react-lite"; // types import { TIssue, ISearchIssueResponse, TIssueKanbanFilters } from "@plane/types"; -import { TCreateModalStoreTypes } from "constants/issue"; +import { KanbanStoreType } from "../base-kanban-root"; interface IHeaderGroupByCard { sub_group_by: string | null; @@ -26,7 +26,7 @@ interface IHeaderGroupByCard { handleKanbanFilters: any; issuePayload: Partial; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; } @@ -56,8 +56,6 @@ export const HeaderGroupByCard: FC = observer((props) => { const isDraftIssue = router.pathname.includes("draft-issue"); - const { setToastAlert } = useToast(); - const renderExistingIssueModal = moduleId || cycleId; const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true }; @@ -67,10 +65,16 @@ export const HeaderGroupByCard: FC = observer((props) => { const issues = data.map((i) => i.id); try { - addIssuesToView && addIssuesToView(issues); + await addIssuesToView?.(issues); + + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the cycle. Please try again.", }); @@ -106,13 +110,21 @@ export const HeaderGroupByCard: FC = observer((props) => { {icon ? icon : }
-
+
{title}
-
+
{count || 0}
diff --git a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx index ea9464780..b0859a70d 100644 --- a/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/sub-group-by-card.tsx @@ -1,7 +1,7 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { Circle, ChevronDown, ChevronUp } from "lucide-react"; // mobx -import { observer } from "mobx-react-lite"; import { TIssueKanbanFilters } from "@plane/types"; interface IHeaderSubGroupByCard { diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index 7cbda05e1..48e92feba 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -3,7 +3,6 @@ import { Droppable } from "@hello-pangea/dnd"; // hooks import { useProjectState } from "hooks/store"; //components -import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; //types import { TGroupedIssues, @@ -13,7 +12,7 @@ import { TSubGroupedIssues, TUnGroupedIssues, } from "@plane/types"; -import { EIssueActions } from "../types"; +import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; interface IKanbanGroup { groupId: string; @@ -25,7 +24,7 @@ interface IKanbanGroup { group_by: string | null; sub_group_id: string; isDragDisabled: boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; quickAddCallback?: ( @@ -53,7 +52,7 @@ export const KanbanGroup = (props: IKanbanGroup) => { issueIds, peekIssueId, isDragDisabled, - handleIssues, + updateIssue, quickActions, canEditProperties, enableQuickIssueCreate, @@ -80,6 +79,10 @@ export const KanbanGroup = (props: IKanbanGroup) => { preloadedData = { ...preloadedData, state_id: groupValue }; } else if (groupByKey === "priority") { preloadedData = { ...preloadedData, priority: groupValue }; + } else if (groupByKey === "cycle") { + preloadedData = { ...preloadedData, cycle_id: groupValue }; + } else if (groupByKey === "module") { + preloadedData = { ...preloadedData, module_ids: [groupValue] }; } else if (groupByKey === "labels" && groupValue != "None") { preloadedData = { ...preloadedData, label_ids: [groupValue] }; } else if (groupByKey === "assignees" && groupValue != "None") { @@ -96,6 +99,10 @@ export const KanbanGroup = (props: IKanbanGroup) => { preloadedData = { ...preloadedData, state_id: subGroupValue }; } else if (subGroupByKey === "priority") { preloadedData = { ...preloadedData, priority: subGroupValue }; + } else if (groupByKey === "cycle") { + preloadedData = { ...preloadedData, cycle_id: subGroupValue }; + } else if (groupByKey === "module") { + preloadedData = { ...preloadedData, module_ids: [subGroupValue] }; } else if (subGroupByKey === "labels" && subGroupValue != "None") { preloadedData = { ...preloadedData, label_ids: [subGroupValue] }; } else if (subGroupByKey === "assignees" && subGroupValue != "None") { @@ -115,9 +122,7 @@ export const KanbanGroup = (props: IKanbanGroup) => { {(provided: any, snapshot: any) => (
@@ -129,7 +134,7 @@ export const KanbanGroup = (props: IKanbanGroup) => { issueIds={(issueIds as TGroupedIssues)?.[groupId] || []} displayProperties={displayProperties} isDragDisabled={isDragDisabled} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} canEditProperties={canEditProperties} scrollableContainerRef={scrollableContainerRef} diff --git a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx index 513163431..71a0e661c 100644 --- a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx @@ -1,19 +1,20 @@ import { useEffect, useState, useRef } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { setPromiseToast } from "@plane/ui"; +import { ISSUE_CREATED } from "constants/event-tracker"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers -import { createIssuePayload } from "helpers/issue.helper"; +// ui // types import { TIssue } from "@plane/types"; // constants -import { ISSUE_CREATED } from "constants/event-tracker"; const Inputs = (props: any) => { const { register, setFocus, projectDetail } = props; @@ -73,7 +74,6 @@ export const KanBanQuickAddIssueForm: React.FC = obser useKeypress("Escape", handleClose); useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); const { reset, @@ -97,46 +97,49 @@ export const KanBanQuickAddIssueForm: React.FC = obser ...formData, }); - try { - quickAddCallback && - (await quickAddCallback( - workspaceSlug.toString(), - projectId.toString(), - { - ...payload, - }, - viewId - ).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback( + workspaceSlug.toString(), + projectId.toString(), + { + ...payload, + }, + viewId + ); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Kanban quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Kanban quick add" }, - path: router.asPath, - }); - console.error(err); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Kanban quick add" }, + path: router.asPath, + }); + }); } }; return ( <> {isOpen ? ( -
+
{ const { workspaceSlug, projectId, cycleId } = router.query; // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { issues } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - }), - [issues, workspaceSlug, cycleId] - ); - const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; @@ -63,10 +35,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { return ( { - const router = useRouter(); - const { workspaceSlug } = router.query; - - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const DraftKanBanLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 07ad7eb83..eaf96a994 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -1,16 +1,14 @@ -import React, { useMemo } from "react"; -import { useRouter } from "next/router"; +import React from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hook +import { ModuleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ModuleIssueQuickActions } from "components/issues"; // types -import { TIssue } from "@plane/types"; // constants -import { EIssueActions } from "../../types"; import { BaseKanBanRoot } from "../base-kanban-root"; -import { EIssuesStoreType } from "constants/issue"; export interface IModuleKanBanLayout {} @@ -19,40 +17,11 @@ export const ModuleKanBanLayout: React.FC = observer(() => { const { workspaceSlug, projectId, moduleId } = router.query; // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - }), - [issues, workspaceSlug, moduleId] - ); + const { issues } = useIssues(EIssuesStoreType.MODULE); return ( { - const router = useRouter(); - const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE); - const { membership: { currentWorkspaceAllProjectsRole }, } = useUser(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - }), - [issues, workspaceSlug, userId] - ); - const canEditPropertiesBasedOnProject = (projectId: string) => { const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; @@ -52,10 +22,7 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => { return ( { - const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const KanBanLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index 8dd33b728..c1a07c317 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -2,39 +2,21 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks -import { useIssues } from "hooks/store"; -// constant import { EIssuesStoreType } from "constants/issue"; +// constant // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; // components import { BaseKanBanRoot } from "../base-kanban-root"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -export interface IViewKanBanLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewKanBanLayout: React.FC = observer((props) => { - const { issueActions } = props; +export const ProjectViewKanBanLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); - return ( void; + storeType: KanbanStoreType; } + +const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { + let headerCount = 0; + Object.keys(issueIds).map((groupState) => { + headerCount = headerCount + (issueIds?.[groupState]?.[groupById]?.length || 0); + }); + return headerCount; +}; + const SubGroupSwimlaneHeader: React.FC = ({ issueIds, sub_group_by, group_by, + storeType, list, kanbanFilters, handleKanbanFilters, }) => ( -
+
{list && list.length > 0 && list.map((_list: IGroupByColumn) => ( -
+
))} @@ -64,13 +75,13 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues; showEmptyGroup: boolean; displayProperties: IIssueDisplayProperties | undefined; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; isDragStarted?: boolean; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; enableQuickIssueCreate: boolean; canEditProperties: (projectId: string | undefined) => boolean; addIssuesToView?: (issueIds: string[]) => Promise; @@ -90,7 +101,8 @@ const SubGroupSwimlane: React.FC = observer((props) => { sub_group_by, group_by, list, - handleIssues, + storeType, + updateIssue, quickActions, displayProperties, kanbanFilters, @@ -120,7 +132,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { {list && list.length > 0 && list.map((_list: any) => ( -
+
= observer((props) => { sub_group_by={sub_group_by} group_by={group_by} sub_group_id={_list.id} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} @@ -156,6 +168,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { viewId={viewId} scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} + storeType={storeType} />
)} @@ -171,14 +184,14 @@ export interface IKanBanSwimLanes { displayProperties: IIssueDisplayProperties | undefined; sub_group_by: string | null; group_by: string | null; - handleIssues: (issue: TIssue, action: EIssueActions) => void; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; showEmptyGroup: boolean; isDragStarted?: boolean; disableIssueCreation?: boolean; - storeType?: TCreateModalStoreTypes; + storeType: KanbanStoreType; addIssuesToView?: (issueIds: string[]) => Promise; enableQuickIssueCreate: boolean; quickAddCallback?: ( @@ -199,7 +212,8 @@ export const KanBanSwimLanes: React.FC = observer((props) => { displayProperties, sub_group_by, group_by, - handleIssues, + updateIssue, + storeType, quickActions, kanbanFilters, handleKanbanFilters, @@ -217,10 +231,28 @@ export const KanBanSwimLanes: React.FC = observer((props) => { const member = useMember(); const project = useProject(); const label = useLabel(); + const cycle = useCycle(); + const projectModule = useModule(); const projectState = useProjectState(); - const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member); - const subGroupByList = getGroupByColumns(sub_group_by as GroupByColumnTypes, project, label, projectState, member); + const groupByList = getGroupByColumns( + group_by as GroupByColumnTypes, + project, + cycle, + projectModule, + label, + projectState, + member + ); + const subGroupByList = getGroupByColumns( + sub_group_by as GroupByColumnTypes, + project, + cycle, + projectModule, + label, + projectState, + member + ); if (!groupByList || !subGroupByList) return null; @@ -234,6 +266,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} list={groupByList} + storeType={storeType} />
@@ -245,7 +278,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { displayProperties={displayProperties} group_by={group_by} sub_group_by={sub_group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} kanbanFilters={kanbanFilters} handleKanbanFilters={handleKanbanFilters} @@ -258,6 +291,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { quickAddCallback={quickAddCallback} viewId={viewId} scrollableContainerRef={scrollableContainerRef} + storeType={storeType} /> )}
diff --git a/web/components/issues/issue-layouts/kanban/utils.ts b/web/components/issues/issue-layouts/kanban/utils.ts index 617598524..855f096e6 100644 --- a/web/components/issues/issue-layouts/kanban/utils.ts +++ b/web/components/issues/issue-layouts/kanban/utils.ts @@ -1,12 +1,5 @@ import { DraggableLocation } from "@hello-pangea/dnd"; -import { ICycleIssues } from "store/issue/cycle"; -import { IDraftIssues } from "store/issue/draft"; -import { IModuleIssues } from "store/issue/module"; -import { IProfileIssues } from "store/issue/profile"; -import { IProjectIssues } from "store/issue/project"; -import { IProjectViewIssues } from "store/issue/project-views"; -import { IWorkspaceIssues } from "store/issue/workspace"; -import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues } from "@plane/types"; +import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues, TIssue } from "@plane/types"; const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => { const sortOrderDefaultValue = 65535; @@ -48,24 +41,16 @@ export const handleDragDrop = async ( destination: DraggableLocation | null | undefined, workspaceSlug: string | undefined, projectId: string | undefined, // projectId for all views or user id in profile issues - store: - | IProjectIssues - | ICycleIssues - | IDraftIssues - | IModuleIssues - | IDraftIssues - | IProjectViewIssues - | IProfileIssues - | IWorkspaceIssues, subGroupBy: string | null, groupBy: string | null, issueMap: IIssueMap, issueWithIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined, - viewId: string | null = null // it can be moduleId, cycleId + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined, + removeIssue: (projectId: string, issueId: string) => Promise | undefined ) => { if (!issueMap || !issueWithIds || !source || !destination || !workspaceSlug || !projectId) return; - let updateIssue: any = {}; + let updatedIssue: any = {}; const sourceDroppableId = source?.droppableId; const destinationDroppableId = destination?.droppableId; @@ -100,8 +85,7 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); if (removed) { - if (viewId) return await store?.removeIssue(workspaceSlug, projectId, removed); //, viewId); - else return await store?.removeIssue(workspaceSlug, projectId, removed); + return await removeIssue(projectId, removed); } } else { //spreading the array to stop changing the original reference @@ -118,14 +102,14 @@ export const handleDragDrop = async ( const [removed] = sourceIssues.splice(source.index, 1); const removedIssueDetail = issueMap[removed]; - updateIssue = { + updatedIssue = { id: removedIssueDetail?.id, project_id: removedIssueDetail?.project_id, }; // for both horizontal and vertical dnd - updateIssue = { - ...updateIssue, + updatedIssue = { + ...updatedIssue, ...handleSortOrder( sourceDroppableId === destinationDroppableId ? sourceIssues : destinationIssues, destination.index, @@ -136,19 +120,19 @@ export const handleDragDrop = async ( if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) { if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) { if (sourceGroupByColumnId != destinationGroupByColumnId) { - if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId }; - if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId }; + if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId }; + if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId }; } } else { if (subGroupBy === "state") - updateIssue = { - ...updateIssue, + updatedIssue = { + ...updatedIssue, state_id: destinationSubGroupByColumnId, priority: destinationGroupByColumnId, }; if (subGroupBy === "priority") - updateIssue = { - ...updateIssue, + updatedIssue = { + ...updatedIssue, state_id: destinationGroupByColumnId, priority: destinationSubGroupByColumnId, }; @@ -156,15 +140,13 @@ export const handleDragDrop = async ( } else { // for horizontal dnd if (sourceColumnId != destinationColumnId) { - if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId }; - if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId }; + if (groupBy === "state") updatedIssue = { ...updatedIssue, state_id: destinationGroupByColumnId }; + if (groupBy === "priority") updatedIssue = { ...updatedIssue, priority: destinationGroupByColumnId }; } } - if (updateIssue && updateIssue?.id) { - if (viewId) - return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue, viewId); - else return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue); + if (updatedIssue && updatedIssue?.id) { + return updateIssue && (await updateIssue(updatedIssue.project_id, updatedIssue.id, updatedIssue)); } } }; diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index ffe9de661..ae198f1ae 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -1,68 +1,46 @@ -import { List } from "./default"; import { FC, useCallback } from "react"; import { observer } from "mobx-react-lite"; // types -import { TIssue } from "@plane/types"; -import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; -import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; -import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { IDraftIssuesFilter, IDraftIssues } from "store/issue/draft"; -import { IArchivedIssuesFilter, IArchivedIssues } from "store/issue/archived"; -import { EIssueActions } from "../types"; -// components -import { IQuickActionProps } from "./list-view-types"; -// constants +import { EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { TCreateModalStoreTypes } from "constants/issue"; -// hooks import { useIssues, useUser } from "hooks/store"; +import { TIssue } from "@plane/types"; +// components +import { List } from "./default"; +import { IQuickActionProps } from "./list-view-types"; +import { useIssuesActions } from "hooks/use-issues-actions"; +// constants +// hooks + +type ListStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW + | EIssuesStoreType.ARCHIVED + | EIssuesStoreType.DRAFT + | EIssuesStoreType.PROFILE; interface IBaseListRoot { - issuesFilter: - | IProjectIssuesFilter - | IModuleIssuesFilter - | ICycleIssuesFilter - | IProjectViewIssuesFilter - | IProfileIssuesFilter - | IDraftIssuesFilter - | IArchivedIssuesFilter; - issues: - | IProjectIssues - | ICycleIssues - | IModuleIssues - | IProjectViewIssues - | IProfileIssues - | IDraftIssues - | IArchivedIssues; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; viewId?: string; - storeType: TCreateModalStoreTypes; + storeType: ListStoreType; addIssuesToView?: (issueIds: string[]) => Promise; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; } - export const BaseListRoot = observer((props: IBaseListRoot) => { const { - issuesFilter, - issues, QuickActions, - issueActions, viewId, storeType, addIssuesToView, canEditPropertiesBasedOnProject, isCompletedCycle = false, } = props; + + const { issuesFilter, issues } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } = useIssuesActions(storeType); // mobx store const { membership: { currentProjectRole }, @@ -80,7 +58,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const isEditingAllowedBasedOnProject = canEditPropertiesBasedOnProject && projectId ? canEditPropertiesBasedOnProject(projectId) : isEditingAllowed; - return enableInlineEditing && isEditingAllowedBasedOnProject; + return !!enableInlineEditing && isEditingAllowedBasedOnProject; }, [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] ); @@ -91,37 +69,20 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const group_by = displayFilters?.group_by || null; const showEmptyGroup = displayFilters?.show_empty_groups ?? false; - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - await issueActions[action]!(issue); - } - }, - [issueActions] - ); - const renderQuickActions = useCallback( (issue: TIssue) => ( handleIssues(issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined - } + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} readOnly={!isEditingAllowed || isCompletedCycle} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps - [handleIssues] + [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); return ( @@ -130,7 +91,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { issuesMap={issueMap} displayProperties={displayProperties} group_by={group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={renderQuickActions} issueIds={issueIds} showEmptyGroup={showEmptyGroup} diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index cc04ed716..099137348 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -1,27 +1,26 @@ import { observer } from "mobx-react-lite"; // components -import { IssueProperties } from "../properties/all-properties"; // hooks -import { useApplication, useIssueDetail, useProject } from "hooks/store"; // ui import { Spinner, Tooltip, ControlLink } from "@plane/ui"; // helper import { cn } from "helpers/common.helper"; +import { useApplication, useIssueDetail, useProject } from "hooks/store"; // types import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; -import { EIssueActions } from "../types"; +import { IssueProperties } from "../properties/all-properties"; interface IssueBlockProps { issueId: string; issuesMap: TIssueMap; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; canEditProperties: (projectId: string | undefined) => boolean; } export const IssueBlock: React.FC = observer((props: IssueBlockProps) => { - const { issuesMap, issueId, handleIssues, quickActions, displayProperties, canEditProperties } = props; + const { issuesMap, issueId, updateIssue, quickActions, displayProperties, canEditProperties } = props; // hooks const { router: { workspaceSlug }, @@ -29,10 +28,6 @@ export const IssueBlock: React.FC = observer((props: IssueBlock const { getProjectIdentifierById } = useProject(); const { peekIssue, setPeekIssue } = useIssueDetail(); - const updateIssue = async (issueToUpdate: TIssue) => { - await handleIssues(issueToUpdate, EIssueActions.UPDATE); - }; - const handleIssuePeekOverview = (issue: TIssue) => workspaceSlug && issue && @@ -65,7 +60,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock )} {issue?.is_draft ? ( - + {issue.name} ) : ( @@ -78,7 +73,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" disabled={!!issue?.tempId} > - + {issue.name} @@ -91,7 +86,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock className="relative flex items-center gap-2 whitespace-nowrap" issue={issue} isReadOnly={!canEditIssueProperties} - handleIssues={updateIssue} + updateIssue={updateIssue} displayProperties={displayProperties} activeLayout="List" /> diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index d3c8d1406..2296e7b68 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -1,23 +1,22 @@ import { FC, MutableRefObject } from "react"; // components +import RenderIfVisible from "components/core/render-if-visible-HOC"; import { IssueBlock } from "components/issues"; // types import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; -import { EIssueActions } from "../types"; -import RenderIfVisible from "components/core/render-if-visible-HOC"; interface Props { issueIds: TGroupedIssues | TUnGroupedIssues | any; issuesMap: TIssueMap; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; containerRef: MutableRefObject; } export const IssueBlocksList: FC = (props) => { - const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties, containerRef } = props; + const { issueIds, issuesMap, updateIssue, quickActions, displayProperties, canEditProperties, containerRef } = props; return (
@@ -35,7 +34,7 @@ export const IssueBlocksList: FC = (props) => { Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; enableIssueQuickAdd: boolean; @@ -36,7 +35,7 @@ export interface IGroupByList { viewId?: string ) => Promise; disableIssueCreation?: boolean; - storeType: TCreateModalStoreTypes; + storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; isCompletedCycle?: boolean; @@ -47,7 +46,7 @@ const GroupByList: React.FC = (props) => { issueIds, issuesMap, group_by, - handleIssues, + updateIssue, quickActions, displayProperties, enableIssueQuickAdd, @@ -65,10 +64,22 @@ const GroupByList: React.FC = (props) => { const project = useProject(); const label = useLabel(); const projectState = useProjectState(); + const cycle = useCycle(); + const projectModule = useModule(); const containerRef = useRef(null); - const groups = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true); + const groups = getGroupByColumns( + group_by as GroupByColumnTypes, + project, + cycle, + projectModule, + label, + projectState, + member, + true, + true + ); if (!groups) return null; @@ -87,6 +98,10 @@ const GroupByList: React.FC = (props) => { preloadedData = { ...preloadedData, label_ids: [value] }; } else if (groupByKey === "assignees" && value != "None") { preloadedData = { ...preloadedData, assignee_ids: [value] }; + } else if (groupByKey === "cycle" && value != "None") { + preloadedData = { ...preloadedData, cycle_id: value }; + } else if (groupByKey === "module" && value != "None") { + preloadedData = { ...preloadedData, module_ids: [value] }; } else if (groupByKey === "created_by") { preloadedData = { ...preloadedData }; } else { @@ -108,7 +123,7 @@ const GroupByList: React.FC = (props) => { const isGroupByCreatedBy = group_by === "created_by"; return ( -
+
{groups && groups.length > 0 && groups.map( @@ -131,7 +146,7 @@ const GroupByList: React.FC = (props) => { Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; showEmptyGroup: boolean; @@ -173,7 +188,7 @@ export interface IList { ) => Promise; viewId?: string; disableIssueCreation?: boolean; - storeType: TCreateModalStoreTypes; + storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; isCompletedCycle?: boolean; } @@ -183,7 +198,7 @@ export const List: React.FC = (props) => { issueIds, issuesMap, group_by, - handleIssues, + updateIssue, quickActions, quickAddCallback, viewId, @@ -203,7 +218,7 @@ export const List: React.FC = (props) => { issueIds={issueIds as TUnGroupedIssues} issuesMap={issuesMap} group_by={group_by} - handleIssues={handleIssues} + updateIssue={updateIssue} quickActions={quickActions} displayProperties={displayProperties} enableIssueQuickAdd={enableIssueQuickAdd} diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index 8d9164b37..fa1a393c4 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -1,19 +1,19 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // lucide icons import { CircleDashed, Plus } from "lucide-react"; // components -import { CreateUpdateIssueModal } from "components/issues"; +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; -import { CustomMenu } from "@plane/ui"; +import { CreateUpdateIssueModal } from "components/issues"; +// ui // mobx -import { observer } from "mobx-react-lite"; // hooks +import { EIssuesStoreType } from "constants/issue"; import { useEventTracker } from "hooks/store"; // types import { TIssue, ISearchIssueResponse } from "@plane/types"; -import useToast from "hooks/use-toast"; -import { useState } from "react"; -import { TCreateModalStoreTypes } from "constants/issue"; interface IHeaderGroupByCard { icon?: React.ReactNode; @@ -21,7 +21,7 @@ interface IHeaderGroupByCard { count: number; issuePayload: Partial; disableIssueCreation?: boolean; - storeType: TCreateModalStoreTypes; + storeType: EIssuesStoreType; addIssuesToView?: (issueIds: string[]) => Promise; } @@ -38,8 +38,6 @@ export const HeaderGroupByCard = observer( const isDraftIssue = router.pathname.includes("draft-issue"); - const { setToastAlert } = useToast(); - const renderExistingIssueModal = moduleId || cycleId; const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true }; @@ -49,10 +47,16 @@ export const HeaderGroupByCard = observer( const issues = data.map((i) => i.id); try { - addIssuesToView && addIssuesToView(issues); + await addIssuesToView?.(issues); + + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Issues added to the cycle successfully.", + }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Selected issues could not be added to the cycle. Please try again.", }); diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx index 8d1ce6d9c..7bae7ecff 100644 --- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -1,19 +1,20 @@ -import { FC, useEffect, useState, useRef, use } from "react"; +import { FC, useEffect, useState, useRef } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { PlusIcon } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks +import { setPromiseToast } from "@plane/ui"; +import { ISSUE_CREATED } from "constants/event-tracker"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// constants -import { TIssue, IProject } from "@plane/types"; +// ui // types -import { createIssuePayload } from "helpers/issue.helper"; +import { TIssue, IProject } from "@plane/types"; +// helper // constants -import { ISSUE_CREATED } from "constants/event-tracker"; interface IInputProps { formKey: string; @@ -77,7 +78,6 @@ export const ListQuickAddIssueForm: FC = observer((props useKeypress("Escape", handleClose); useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); const { reset, @@ -101,31 +101,35 @@ export const ListQuickAddIssueForm: FC = observer((props ...formData, }); - try { - quickAddCallback && - (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { + if (quickAddCallback) { + const quickAddPromise = quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, + }); + + await quickAddPromise + .then((res) => { captureIssueEvent({ eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "List quick add" }, path: router.asPath, }); - })); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "List quick add" }, - path: router.asPath, - }); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", - }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "List quick add" }, + path: router.asPath, + }); + }); } }; diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index 6e70d00d0..73f8e3d3b 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -1,47 +1,20 @@ -import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useIssues } from "hooks/store"; -// components import { ArchivedIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; +// components // types -import { TIssue } from "@plane/types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssueActions } from "../../types"; -import { EIssuesStoreType } from "constants/issue"; export const ArchivedIssueListLayout: FC = observer(() => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); - const issueActions = useMemo( - () => ({ - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - [EIssueActions.RESTORE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.restoreIssue(workspaceSlug, projectId, issue.id); - }, - }), - [issues, workspaceSlug, projectId] - ); - const canEditPropertiesBasedOnProject = () => false; return ( ); diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index 5c15ebe60..26afdf25b 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -1,16 +1,14 @@ -import React, { useCallback, useMemo } from "react"; -import { useRouter } from "next/router"; +import React, { useCallback } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks +import { CycleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useCycle, useIssues } from "hooks/store"; // components -import { CycleIssueQuickActions } from "components/issues"; // types -import { TIssue } from "@plane/types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssueActions } from "../../types"; -import { EIssuesStoreType } from "constants/issue"; export interface ICycleListLayout {} @@ -18,34 +16,9 @@ export const CycleListLayout: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { issues } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCompletedCycleIds } = useCycle(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); - }, - }), - [issues, workspaceSlug, cycleId] - ); const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; @@ -61,10 +34,7 @@ export const CycleListLayout: React.FC = observer(() => { return ( { const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = router.query; if (!workspaceSlug || !projectId) return null; - // store - const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - }), - [issues, workspaceSlug, projectId] - ); - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 95c62d34c..3c6a8894a 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -1,16 +1,14 @@ -import React, { useMemo } from "react"; -import { useRouter } from "next/router"; +import React from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // mobx store +import { ModuleIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; import { useIssues } from "hooks/store"; // components -import { ModuleIssueQuickActions } from "components/issues"; // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssuesStoreType } from "constants/issue"; export interface IModuleListLayout {} @@ -18,40 +16,11 @@ export const ModuleListLayout: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); - }, - }), - [issues, workspaceSlug, moduleId] - ); + const { issues } = useIssues(EIssuesStoreType.MODULE); return ( { diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index fa4a05bbc..f24683d95 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -1,50 +1,20 @@ -import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useIssues, useUser } from "hooks/store"; -// components import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +import { useUser } from "hooks/store"; +// components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; export const ProfileIssuesListLayout: FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string }; - // store hooks - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE); - const { membership: { currentWorkspaceAllProjectsRole }, } = useUser(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, userId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !userId) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId); - }, - }), - [issues, workspaceSlug, userId] - ); - const canEditPropertiesBasedOnProject = (projectId: string) => { const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; @@ -53,10 +23,7 @@ export const ProfileIssuesListLayout: FC = observer(() => { return ( diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 9e1b5830b..fbbd26ffb 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -1,55 +1,19 @@ -import { FC, useMemo } from "react"; -import { useRouter } from "next/router"; +import { FC } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useIssues } from "hooks/store"; -// components import { ProjectIssueQuickActions } from "components/issues"; +import { EIssuesStoreType } from "constants/issue"; +// components // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { EIssuesStoreType } from "constants/issue"; export const ListLayout: FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId } = router.query; if (!workspaceSlug || !projectId) return null; - // store - const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug, projectId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.archiveIssue(workspaceSlug, projectId, issue.id); - }, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [issues] - ); - - return ( - - ); + return ; }); diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index 5ecfd6da2..260dd54bd 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -2,30 +2,14 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // store -import { useIssues } from "hooks/store"; -// constants import { EIssuesStoreType } from "constants/issue"; +// constants // types -import { EIssueActions } from "../../types"; -import { TIssue } from "@plane/types"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; // components import { BaseListRoot } from "../base-list-root"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; - -export interface IViewListLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewListLayout: React.FC = observer((props) => { - const { issueActions } = props; - // store - const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT_VIEW); +export const ProjectViewListLayout: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, viewId } = router.query; @@ -33,10 +17,7 @@ export const ProjectViewListLayout: React.FC = observer((props) return ( diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 776a1cd46..8c1e33b8c 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -1,14 +1,10 @@ import { useCallback, useMemo } from "react"; +import xor from "lodash/xor"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react"; -import xor from "lodash/xor"; // hooks -import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store"; -// components -import { IssuePropertyLabels } from "../properties/labels"; import { Tooltip } from "@plane/ui"; -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { DateDropdown, EstimateDropdown, @@ -18,19 +14,23 @@ import { CycleDropdown, StateDropdown, } from "components/dropdowns"; -// helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; -import { cn } from "helpers/common.helper"; -// types -import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; -// constants import { ISSUE_UPDATED } from "constants/event-tracker"; import { EIssuesStoreType } from "constants/issue"; +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; +import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store"; +// components +import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; +import { IssuePropertyLabels } from "../properties/labels"; +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +// helpers +// types +// constants export interface IIssueProperties { issue: TIssue; - handleIssues: (issue: TIssue) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; displayProperties: IIssueDisplayProperties | undefined; isReadOnly: boolean; className: string; @@ -38,7 +38,7 @@ export interface IIssueProperties { } export const IssueProperties: React.FC = observer((props) => { - const { issue, handleIssues, displayProperties, activeLayout, isReadOnly, className } = props; + const { issue, updateIssue, displayProperties, activeLayout, isReadOnly, className } = props; // store hooks const { labelMap } = useLabel(); const { captureIssueEvent } = useEventTracker(); @@ -52,7 +52,7 @@ export const IssueProperties: React.FC = observer((props) => { const { getStateById } = useProjectState(); // router const router = useRouter(); - const { workspaceSlug, cycleId, moduleId } = router.query; + const { workspaceSlug } = router.query; const currentLayout = `${activeLayout} layout`; // derived values const stateDetails = getStateById(issue.state_id); @@ -80,59 +80,63 @@ export const IssueProperties: React.FC = observer((props) => { ); const handleState = (stateId: string) => { - handleIssues({ ...issue, state_id: stateId }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "state", - change_details: stateId, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { state_id: stateId }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "state", + change_details: stateId, + }, + }); }); - }); }; const handlePriority = (value: TIssuePriorities) => { - handleIssues({ ...issue, priority: value }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "priority", - change_details: value, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { priority: value }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "priority", + change_details: value, + }, + }); }); - }); }; const handleLabel = (ids: string[]) => { - handleIssues({ ...issue, label_ids: ids }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "labels", - change_details: ids, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { label_ids: ids }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "labels", + change_details: ids, + }, + }); }); - }); }; const handleAssignee = (ids: string[]) => { - handleIssues({ ...issue, assignee_ids: ids }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "assignees", - change_details: ids, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { assignee_ids: ids }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "assignees", + change_details: ids, + }, + }); }); - }); }; const handleModule = useCallback( @@ -175,45 +179,52 @@ export const IssueProperties: React.FC = observer((props) => { ); const handleStartDate = (date: Date | null) => { - handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "start_date", - change_details: date ? renderFormattedPayloadDate(date) : null, - }, - }); - }); + updateIssue && + updateIssue(issue.project_id, issue.id, { start_date: date ? renderFormattedPayloadDate(date) : null }).then( + () => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "start_date", + change_details: date ? renderFormattedPayloadDate(date) : null, + }, + }); + } + ); }; const handleTargetDate = (date: Date | null) => { - handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "target_date", - change_details: date ? renderFormattedPayloadDate(date) : null, - }, - }); - }); + updateIssue && + updateIssue(issue.project_id, issue.id, { target_date: date ? renderFormattedPayloadDate(date) : null }).then( + () => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "target_date", + change_details: date ? renderFormattedPayloadDate(date) : null, + }, + }); + } + ); }; const handleEstimate = (value: number | null) => { - handleIssues({ ...issue, estimate_point: value }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: router.asPath, - updates: { - changed_property: "estimate_point", - change_details: value, - }, + updateIssue && + updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: router.asPath, + updates: { + changed_property: "estimate_point", + change_details: value, + }, + }); }); - }); }; const redirectToIssueDetail = () => { @@ -240,7 +251,7 @@ export const IssueProperties: React.FC = observer((props) => { {/* basic properties */} {/* state */} -
+
= observer((props) => { {/* modules */} - {moduleId === undefined && ( - -
- -
-
- )} + +
+ +
+
{/* cycles */} - {cycleId === undefined && ( - -
- -
-
- )} + +
+ +
+
{/* estimates */} {areEstimatesEnabledForCurrentProject && ( diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index 0c1091d39..090f0ce56 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -1,16 +1,16 @@ import { Fragment, useEffect, useRef, useState } from "react"; +import { Placement } from "@popperjs/core"; import { observer } from "mobx-react-lite"; import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, Search, Tags } from "lucide-react"; // hooks +import { Tooltip } from "@plane/ui"; import { useApplication, useLabel } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { Combobox } from "@headlessui/react"; -import { Tooltip } from "@plane/ui"; // types -import { Placement } from "@popperjs/core"; import { IIssueLabel } from "@plane/types"; export interface IIssuePropertyLabels { @@ -56,7 +56,7 @@ export const IssuePropertyLabels: React.FC = observer((pro // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); // store hooks const { router: { workspaceSlug }, @@ -149,7 +149,7 @@ export const IssuePropertyLabels: React.FC = observer((pro {projectLabels ?.filter((l) => value.includes(l?.id)) .map((label) => ( - +
= observer((pro disabled ? "cursor-not-allowed text-custom-text-200" : value.length <= maxRender - ? "cursor-pointer" - : "cursor-pointer hover:bg-custom-background-80" + ? "cursor-pointer" + : "cursor-pointer hover:bg-custom-background-80" } ${buttonClassName}`} onClick={handleOnClick} > diff --git a/web/components/issues/issue-layouts/properties/with-display-properties-HOC.tsx b/web/components/issues/issue-layouts/properties/with-display-properties-HOC.tsx index d7d32b85d..937c14983 100644 --- a/web/components/issues/issue-layouts/properties/with-display-properties-HOC.tsx +++ b/web/components/issues/issue-layouts/properties/with-display-properties-HOC.tsx @@ -1,5 +1,5 @@ -import { observer } from "mobx-react-lite"; import { ReactNode } from "react"; +import { observer } from "mobx-react-lite"; import { IIssueDisplayProperties } from "@plane/types"; interface IWithDisplayPropertiesHOC { diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 1d0472454..f6c63191f 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -1,22 +1,23 @@ import { useState } from "react"; -import { useRouter } from "next/router"; -import { ArchiveIcon, CustomMenu } from "@plane/ui"; -import { observer } from "mobx-react"; -import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; +import { observer } from "mobx-react"; +import { useRouter } from "next/router"; +import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; // hooks -import useToast from "hooks/use-toast"; +import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// ui +// components +import { EIssuesStoreType } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { useEventTracker, useProjectState } from "hooks/store"; // components -import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers -import { copyUrlToClipboard } from "helpers/string.helper"; // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constants -import { EIssuesStoreType } from "constants/issue"; -import { STATE_GROUPS } from "constants/state"; export const AllIssueQuickActions: React.FC = observer((props) => { const { @@ -39,8 +40,6 @@ export const AllIssueQuickActions: React.FC = observer((props // store hooks const { setTrackElement } = useEventTracker(); const { getStateById } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); const isEditingAllowed = !readOnly; @@ -54,8 +53,8 @@ export const AllIssueQuickActions: React.FC = observer((props const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) @@ -91,11 +90,12 @@ export const AllIssueQuickActions: React.FC = observer((props }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.PROJECT} /> = (props) => { const { issue, handleDelete, handleRestore, customActionButton, portalElement, readOnly = false } = props; @@ -32,16 +34,14 @@ export const ArchivedIssueQuickActions: React.FC = (props) => // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; const isRestoringAllowed = handleRestore && isEditingAllowed; - // toast alert - const { setToastAlert } = useToast(); const issueLink = `${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`; const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) @@ -56,6 +56,7 @@ export const ArchivedIssueQuickActions: React.FC = (props) => onSubmit={handleDelete} /> = observer((props) => { const { @@ -45,8 +47,6 @@ export const CycleIssueQuickActions: React.FC = observer((pro membership: { currentProjectRole }, } = useUser(); const { getStateById } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); // auth @@ -64,8 +64,8 @@ export const CycleIssueQuickActions: React.FC = observer((pro const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) @@ -101,11 +101,12 @@ export const CycleIssueQuickActions: React.FC = observer((pro }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.CYCLE} /> = observer((props) => { const { @@ -45,8 +46,6 @@ export const ModuleIssueQuickActions: React.FC = observer((pr membership: { currentProjectRole }, } = useUser(); const { getStateById } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); // auth @@ -64,8 +63,8 @@ export const ModuleIssueQuickActions: React.FC = observer((pr const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) @@ -101,11 +100,12 @@ export const ModuleIssueQuickActions: React.FC = observer((pr }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.MODULE} /> = observer((props) => { const { @@ -54,16 +54,14 @@ export const ProjectIssueQuickActions: React.FC = observer((p !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; - const { setToastAlert } = useToast(); - const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) @@ -102,12 +100,13 @@ export const ProjectIssueQuickActions: React.FC = observer((p }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.PROJECT} isDraft={isDraftIssue} /> { // router const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; - // theme - const { resolvedTheme } = useTheme(); + const { workspaceSlug, globalViewId, ...routeFilters } = router.query; //swr hook for fetching issue properties useWorkspaceIssueProperties(workspaceSlug); // store const { commandPalette: commandPaletteStore } = useApplication(); const { issuesFilter: { filters, fetchFilters, updateFilters }, - issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue, archiveIssue }, + issues: { loader, groupedIssueIds, fetchIssues }, } = useIssues(EIssuesStoreType.GLOBAL); + const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); const { dataViewId, issueIds } = groupedIssueIds; const { - membership: { currentWorkspaceAllProjectsRole, currentWorkspaceRole }, - currentUser, + membership: { currentWorkspaceAllProjectsRole }, } = useUser(); const { fetchAllGlobalViews } = useGlobalView(); const { workspaceProjectIds } = useProject(); @@ -48,10 +44,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId); const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view"; - const currentViewDetails = ALL_ISSUES_EMPTY_STATE_DETAILS[currentView as keyof typeof ALL_ISSUES_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode); // filter init from the query params @@ -61,14 +53,10 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { globalViewId && ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString()) ) { - const routerQueryParams = { ...router.query }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { ["workspaceSlug"]: _workspaceSlug, ["globalViewId"]: _globalViewId, ...filters } = routerQueryParams; - let issueFilters: any = {}; - Object.keys(filters).forEach((key) => { + Object.keys(routeFilters).forEach((key) => { const filterKey: any = key; - const filterValue = filters[key]?.toString() || undefined; + const filterValue = routeFilters[key]?.toString() || undefined; if ( ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet.filters.includes(filterKey) && filterKey && @@ -77,7 +65,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { issueFilters = { ...issueFilters, [filterKey]: filterValue.split(",") }; }); - if (!isEmpty(filters)) + if (!isEmpty(routeFilters)) updateFilters( workspaceSlug.toString(), undefined, @@ -124,41 +112,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - const projectId = issue.project_id; - if (!workspaceSlug || !projectId || !globalViewId) return; - - await updateIssue(workspaceSlug.toString(), projectId, issue.id, issue, globalViewId.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - const projectId = issue.project_id; - if (!workspaceSlug || !projectId || !globalViewId) return; - - await removeIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString()); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - const projectId = issue.project_id; - if (!workspaceSlug || !projectId || !globalViewId) return; - - await archiveIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString()); - }, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [updateIssue, removeIssue, workspaceSlug] - ); - - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (action === EIssueActions.UPDATE) await issueActions[action]!(issue); - if (action === EIssueActions.DELETE) await issueActions[action]!(issue); - if (action === EIssueActions.ARCHIVE) await issueActions[action]!(issue); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - const handleDisplayFiltersUpdate = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !globalViewId) return; @@ -179,56 +132,44 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { handleIssues({ ...issue }, EIssueActions.UPDATE)} - handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)} - handleArchive={async () => handleIssues(issue, EIssueActions.ARCHIVE)} + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} portalElement={portalElement} readOnly={!canEditProperties(issue.project_id)} /> ), - [canEditProperties, handleIssues] + [canEditProperties, removeIssue, updateIssue, archiveIssue] ); - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) { return ; } + const emptyStateType = + (workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS; + return (
{issueIds.length === 0 ? ( 0 ? currentViewDetails.title : "No project"} - description={ - (workspaceProjectIds ?? []).length > 0 - ? currentViewDetails.description - : "To create issues or manage your work, you need to create a project or be a part of one." - } + type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} size="sm" - primaryButton={ + primaryButtonOnClick={ (workspaceProjectIds ?? []).length > 0 ? currentView !== "custom-view" && currentView !== "subscribed" - ? { - text: "Create new issue", - onClick: () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, + ? () => { + setTrackElement("All issues empty state"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); } : undefined - : { - text: "Start your first project", - onClick: () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - }, + : () => { + setTrackElement("All issues empty state"); + commandPaletteStore.toggleCreateProjectModal(true); } } - disabled={!isEditingAllowed} /> ) : ( @@ -238,7 +179,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { handleDisplayFilterUpdate={handleDisplayFiltersUpdate} issueIds={issueIds} quickActions={renderQuickActions} - handleIssues={handleIssues} + updateIssue={updateIssue} canEditProperties={canEditProperties} viewId={globalViewId} /> diff --git a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx index 7db9a1e3b..ae8ca400a 100644 --- a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx @@ -1,9 +1,8 @@ import React, { Fragment } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // mobx store -import { useIssues } from "hooks/store"; // components import { ArchivedIssueListLayout, @@ -11,9 +10,10 @@ import { ProjectArchivedEmptyState, IssuePeekOverview, } from "components/issues"; +import { ListLayoutLoader } from "components/ui"; import { EIssuesStoreType } from "constants/issue"; // ui -import { ListLayoutLoader } from "components/ui"; +import { useIssues } from "hooks/store"; export const ArchivedIssueLayoutRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 759495284..5f308fbd1 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -1,12 +1,12 @@ import React, { Fragment, useState } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; -import size from "lodash/size"; import isEmpty from "lodash/isEmpty"; +import size from "lodash/size"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // hooks -import { useCycle, useIssues } from "hooks/store"; // components +import { TransferIssues, TransferIssuesModal } from "components/cycles"; import { CycleAppliedFiltersRoot, CycleCalendarLayout, @@ -17,10 +17,10 @@ import { CycleSpreadsheetLayout, IssuePeekOverview, } from "components/issues"; -import { TransferIssues, TransferIssuesModal } from "components/cycles"; import { ActiveLoader } from "components/ui"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { useCycle, useIssues } from "hooks/store"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx index 02b666ceb..1a1602ad1 100644 --- a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx @@ -1,19 +1,19 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks -import { useIssues } from "hooks/store"; -// components -import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue"; -import { DraftIssueListLayout } from "../list/roots/draft-issue-root"; -import { ProjectDraftEmptyState } from "../empty-states"; import { IssuePeekOverview } from "components/issues/peek-overview"; import { ActiveLoader } from "components/ui"; -// ui -import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root"; -// constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; +// components +import { ProjectDraftEmptyState } from "../empty-states"; +import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue"; +import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root"; +import { DraftIssueListLayout } from "../list/roots/draft-issue-root"; +// ui +// constants export const DraftIssueLayoutRoot: React.FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index 14505c65a..0c6ba3b66 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -1,10 +1,9 @@ import React, { Fragment } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; import size from "lodash/size"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import useSWR from "swr"; // mobx store -import { useIssues } from "hooks/store"; // components import { IssuePeekOverview, @@ -19,6 +18,7 @@ import { import { ActiveLoader } from "components/ui"; // constants import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; // types import { IIssueFilterOptions } from "@plane/types"; diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index cae73610e..a57d73b2c 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -1,8 +1,10 @@ import { FC, Fragment } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // components +// ui +import { Spinner } from "@plane/ui"; import { ListLayout, CalendarLayout, @@ -13,14 +15,12 @@ import { ProjectEmptyState, IssuePeekOverview, } from "components/issues"; -// ui -import { Spinner } from "@plane/ui"; // hooks -import { useIssues } from "hooks/store"; // helpers import { ActiveLoader } from "components/ui"; // constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; export const ProjectLayoutRoot: FC = observer(() => { // router diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index fa942b7f6..d15e65865 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -1,9 +1,8 @@ -import React, { Fragment, useMemo } from "react"; -import { useRouter } from "next/router"; +import React, { Fragment } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // mobx store -import { useIssues } from "hooks/store"; // components import { IssuePeekOverview, @@ -18,9 +17,8 @@ import { import { ActiveLoader } from "components/ui"; // constants import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store"; // types -import { TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; export const ProjectViewLayoutRoot: React.FC = observer(() => { // router @@ -45,22 +43,6 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => { { revalidateIfStale: false, revalidateOnFocus: false } ); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue, viewId?.toString()); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !projectId) return; - - await issues.removeIssue(workspaceSlug.toString(), projectId.toString(), issue.id, viewId?.toString()); - }, - }), - [issues, workspaceSlug, projectId, viewId] - ); - const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; if (!workspaceSlug || !projectId || !viewId) return <>; @@ -81,15 +63,15 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
{activeLayout === "list" ? ( - + ) : activeLayout === "kanban" ? ( - + ) : activeLayout === "calendar" ? ( - + ) : activeLayout === "gantt_chart" ? ( - + ) : activeLayout === "spreadsheet" ? ( - + ) : null}
diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 2f09b55d6..653cc28f2 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -1,57 +1,45 @@ import { FC, useCallback } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useUser } from "hooks/store"; -// views -import { SpreadsheetView } from "./spreadsheet-view"; -// types -import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; -import { EIssueActions } from "../types"; -import { IQuickActionProps } from "../list/list-view-types"; -// constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; -import { ICycleIssuesFilter, ICycleIssues } from "store/issue/cycle"; -import { IModuleIssuesFilter, IModuleIssues } from "store/issue/module"; -import { IProjectIssuesFilter, IProjectIssues } from "store/issue/project"; -import { IProjectViewIssuesFilter, IProjectViewIssues } from "store/issue/project-views"; -import { EIssueFilterType } from "constants/issue"; +import { useIssues, useUser } from "hooks/store"; +import { useIssuesActions } from "hooks/use-issues-actions"; +// views +// types +// constants +import { TIssue, IIssueDisplayFilterOptions, TUnGroupedIssues } from "@plane/types"; +import { IQuickActionProps } from "../list/list-view-types"; +import { SpreadsheetView } from "./spreadsheet-view"; +export type SpreadsheetStoreType = + | EIssuesStoreType.PROJECT + | EIssuesStoreType.MODULE + | EIssuesStoreType.CYCLE + | EIssuesStoreType.PROJECT_VIEW; interface IBaseSpreadsheetRoot { - issueFiltersStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; - issueStore: IProjectIssues | ICycleIssues | IModuleIssues | IProjectViewIssues; viewId?: string; QuickActions: FC; - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => void; - [EIssueActions.UPDATE]?: (issue: TIssue) => void; - [EIssueActions.REMOVE]?: (issue: TIssue) => void; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => void; - [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; - }; + storeType: SpreadsheetStoreType; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; } export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { - const { - issueFiltersStore, - issueStore, - viewId, - QuickActions, - issueActions, - canEditPropertiesBasedOnProject, - isCompletedCycle = false, - } = props; + const { viewId, QuickActions, storeType, canEditPropertiesBasedOnProject, isCompletedCycle = false } = props; // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { projectId } = router.query; // store hooks const { membership: { currentProjectRole }, } = useUser(); + const { issues, issuesFilter } = useIssues(storeType); + const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue, updateFilters } = + useIssuesActions(storeType); // derived values - const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {}; + const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {}; // user role validation const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -65,32 +53,17 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] ); - const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues; - - const handleIssues = useCallback( - async (issue: TIssue, action: EIssueActions) => { - if (issueActions[action]) { - issueActions[action]!(issue); - } - }, - [issueActions] - ); + const issueIds = (issues.groupedIssueIds ?? []) as TUnGroupedIssues; const handleDisplayFiltersUpdate = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; + if (!projectId) return; - issueFiltersStore.updateFilters( - workspaceSlug, - projectId, - EIssueFilterType.DISPLAY_FILTERS, - { - ...updatedDisplayFilter, - }, - viewId - ); + updateFilters(projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { + ...updatedDisplayFilter, + }); }, - [issueFiltersStore?.updateFilters, projectId, workspaceSlug, viewId] + [projectId, updateFilters] ); const renderQuickActions = useCallback( @@ -98,37 +71,28 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { handleIssues(issue, EIssueActions.DELETE)} - handleUpdate={ - issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined - } - handleRemoveFromView={ - issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined - } - handleArchive={ - issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined - } - handleRestore={ - issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined - } + handleDelete={async () => removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleRemoveFromView={async () => removeIssueFromView && removeIssueFromView(issue.project_id, issue.id)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + handleRestore={async () => restoreIssue && restoreIssue(issue.project_id, issue.id)} portalElement={portalElement} readOnly={!isEditingAllowed || isCompletedCycle} /> ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [handleIssues] + [isEditingAllowed, isCompletedCycle, removeIssue, updateIssue, removeIssueFromView, archiveIssue, restoreIssue] ); return ( = observer((props) => { buttonClassName="relative border-[0.5px] border-custom-border-400 h-4.5" onClose={onClose} multiple - showCount={true} + showCount showTooltip />
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx index b8801559c..714134d0c 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx @@ -8,7 +8,7 @@ import { TIssue } from "@plane/types"; type Props = { issue: TIssue; onClose: () => void; - onChange: (issue: TIssue, data: Partial,updates:any) => void; + onChange: (issue: TIssue, data: Partial, updates: any) => void; disabled: boolean; }; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx index c635ca85e..85e294641 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx @@ -2,11 +2,11 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks +import { cn } from "helpers/common.helper"; import { useApplication } from "hooks/store"; // types import { TIssue } from "@plane/types"; // helpers -import { cn } from "helpers/common.helper"; type Props = { issue: TIssue; diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx index 3ce70868d..825c2b31c 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx @@ -1,26 +1,25 @@ import { useRef } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; // types -import { IIssueDisplayProperties, TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; -// constants import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; -// components -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { useEventTracker } from "hooks/store"; -import { observer } from "mobx-react"; +import { IIssueDisplayProperties, TIssue } from "@plane/types"; +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +// constants +// components type Props = { displayProperties: IIssueDisplayProperties; issueDetail: TIssue; disableUserActions: boolean; property: keyof IIssueDisplayProperties; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; isEstimateEnabled: boolean; }; export const IssueColumn = observer((props: Props) => { - const { displayProperties, issueDetail, disableUserActions, property, handleIssues, isEstimateEnabled } = props; + const { displayProperties, issueDetail, disableUserActions, property, updateIssue, isEstimateEnabled } = props; // router const router = useRouter(); const tableCellRef = useRef(null); @@ -44,7 +43,8 @@ export const IssueColumn = observer((props: Props) => { , updates: any) => - handleIssues({ ...issue, ...data }, EIssueActions.UPDATE).then(() => { + updateIssue && + updateIssue(issue.project_id, issue.id, data).then(() => { captureIssueEvent({ eventName: "Issue updated", payload: { diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 9f4810c78..8a8ce29f4 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -1,24 +1,24 @@ import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // icons import { ChevronRight, MoreHorizontal } from "lucide-react"; -// constants -import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; -// components -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; -import RenderIfVisible from "components/core/render-if-visible-HOC"; -import { IssueColumn } from "./issue-column"; // ui import { ControlLink, Tooltip } from "@plane/ui"; -// hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { useIssueDetail, useProject } from "hooks/store"; +// components +import RenderIfVisible from "components/core/render-if-visible-HOC"; +// constants +import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; // helper import { cn } from "helpers/common.helper"; +// hooks +import { useIssueDetail, useProject } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types import { IIssueDisplayProperties, TIssue } from "@plane/types"; -import { EIssueActions } from "../types"; +// local components +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +import { IssueColumn } from "./issue-column"; interface Props { displayProperties: IIssueDisplayProperties; @@ -29,7 +29,7 @@ interface Props { portalElement?: HTMLDivElement | null ) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; portalElement: React.MutableRefObject; nestingLevel: number; issueId: string; @@ -45,7 +45,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { isEstimateEnabled, nestingLevel, portalElement, - handleIssues, + updateIssue, quickActions, canEditProperties, isScrolled, @@ -75,7 +75,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { canEditProperties={canEditProperties} nestingLevel={nestingLevel} isEstimateEnabled={isEstimateEnabled} - handleIssues={handleIssues} + updateIssue={updateIssue} portalElement={portalElement} isScrolled={isScrolled} isExpanded={isExpanded} @@ -95,7 +95,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => { canEditProperties={canEditProperties} nestingLevel={nestingLevel + 1} isEstimateEnabled={isEstimateEnabled} - handleIssues={handleIssues} + updateIssue={updateIssue} portalElement={portalElement} isScrolled={isScrolled} containerRef={containerRef} @@ -115,7 +115,7 @@ interface IssueRowDetailsProps { portalElement?: HTMLDivElement | null ) => React.ReactNode; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; portalElement: React.MutableRefObject; nestingLevel: number; issueId: string; @@ -131,7 +131,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { isEstimateEnabled, nestingLevel, portalElement, - handleIssues, + updateIssue, quickActions, canEditProperties, isScrolled, @@ -241,9 +241,9 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { disabled={!!issueDetail?.tempId} >
- +
{issueDetail.name} @@ -255,11 +255,12 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { {/* Rest of the columns */} {SPREADSHEET_PROPERTY_LIST.map((property) => ( ))} diff --git a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx index 3cba3c6cd..6b886ffa9 100644 --- a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx @@ -1,19 +1,20 @@ import { useEffect, useState, useRef } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // hooks +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; +import { ISSUE_CREATED } from "constants/event-tracker"; +import { createIssuePayload } from "helpers/issue.helper"; import { useEventTracker, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers -import { createIssuePayload } from "helpers/issue.helper"; +// ui // types import { TIssue } from "@plane/types"; // constants -import { ISSUE_CREATED } from "constants/event-tracker"; type Props = { formKey: keyof TIssue; @@ -84,7 +85,6 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => // hooks useKeypress("Escape", handleClose); useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); useEffect(() => { setFocus("name"); @@ -100,13 +100,13 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => Object.keys(errors).forEach((key) => { const error = errors[key as keyof TIssue]; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.message?.toString() || "Some error occurred. Please try again.", }); }); - }, [errors, setToastAlert]); + }, [errors]); // const onSubmitHandler = async (formData: TIssue) => { // if (isSubmitting || !workspaceSlug || !projectId) return; @@ -130,8 +130,8 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => // payload // ); - // setToastAlert({ - // type: "success", + // setToast({ + // type: TOAST_TYPE.SUCCESS, // title: "Success!", // message: "Issue created successfully.", // }); @@ -140,8 +140,8 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => // const error = err?.[key]; // const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null; - // setToastAlert({ - // type: "error", + // setToast({ + // type: TOAST_TYPE.ERROR, // title: "Error!", // message: errorTitle || "Some error occurred. Please try again.", // }); @@ -159,34 +159,41 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => ...formData, }); - try { - quickAddCallback && - (await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then( - (res) => { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" }, - path: router.asPath, - }); - } - )); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - captureIssueEvent({ - eventName: ISSUE_CREATED, - payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" }, - path: router.asPath, - }); - console.error(err); - setToastAlert({ - type: "error", - title: "Error!", - message: err?.message || "Some error occurred. Please try again.", + if (quickAddCallback) { + const quickAddPromise = quickAddCallback( + currentWorkspace.slug, + currentProjectDetails.id, + { ...payload } as TIssue, + viewId + ); + setPromiseToast(quickAddPromise, { + loading: "Adding issue...", + success: { + title: "Success!", + message: () => "Issue created successfully.", + }, + error: { + title: "Error!", + message: (err) => err?.message || "Some error occurred. Please try again.", + }, }); + + await quickAddPromise + .then((res) => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" }, + path: router.asPath, + }); + }) + .catch((err) => { + captureIssueEvent({ + eventName: ISSUE_CREATED, + payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" }, + path: router.asPath, + }); + console.error(err); + }); } }; diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx index 6d3037de0..b8b4fd08a 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx @@ -1,59 +1,32 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store -import { useCycle, useIssues } from "hooks/store"; -// components -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -import { EIssueActions } from "../../types"; -import { TIssue } from "@plane/types"; -import { CycleIssueQuickActions } from "../../quick-action-dropdowns"; import { EIssuesStoreType } from "constants/issue"; +import { useCycle } from "hooks/store"; +// components +import { CycleIssueQuickActions } from "../../quick-action-dropdowns"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const CycleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { cycleId } = router.query; const { currentProjectCompletedCycleIds } = useCycle(); - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - - issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, cycleId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - issues.removeIssue(workspaceSlug, issue.project_id, issue.id, cycleId); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - issues.removeIssueFromCycle(workspaceSlug, issue.project_id, cycleId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !cycleId) return; - issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, cycleId); - }, - }), - [issues, workspaceSlug, cycleId] - ); - const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]); + if (!cycleId) return null; + return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx index af8abc801..a95919cdc 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx @@ -1,51 +1,23 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store -import { useIssues } from "hooks/store"; -// components -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -import { EIssueActions } from "../../types"; -import { TIssue } from "@plane/types"; -import { ModuleIssueQuickActions } from "../../quick-action-dropdowns"; import { EIssuesStoreType } from "constants/issue"; +// components +import { ModuleIssueQuickActions } from "../../quick-action-dropdowns"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const ModuleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string }; + const { moduleId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - - issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - issues.removeIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - [EIssueActions.REMOVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug || !moduleId) return; - issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId); - }, - }), - [issues, workspaceSlug, moduleId] - ); + if (!moduleId) return null; return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx index 4ce54cff5..dc9d354a6 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx @@ -1,48 +1,10 @@ -import React, { useMemo } from "react"; +import React from "react"; import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; // mobx store -import { useIssues } from "hooks/store"; - -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -import { EIssueActions } from "../../types"; -import { TIssue } from "@plane/types"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; import { EIssuesStoreType } from "constants/issue"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -export const ProjectSpreadsheetLayout: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string }; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const issueActions = useMemo( - () => ({ - [EIssueActions.UPDATE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); - }, - [EIssueActions.ARCHIVE]: async (issue: TIssue) => { - if (!workspaceSlug) return; - - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); - }, - }), - [issues, workspaceSlug] - ); - - return ( - - ); -}); +export const ProjectSpreadsheetLayout: React.FC = observer(() => ( + +)); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx index d8b7571e5..754d87c2f 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx @@ -2,40 +2,23 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store -import { useIssues } from "hooks/store"; -// components -import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; -import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -// types -import { EIssueActions } from "../../types"; -import { TIssue } from "@plane/types"; -// constants import { EIssuesStoreType } from "constants/issue"; +// components +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; +// types +// constants -export interface IViewSpreadsheetLayout { - issueActions: { - [EIssueActions.DELETE]: (issue: TIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; - [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; - }; -} - -export const ProjectViewSpreadsheetLayout: React.FC = observer((props) => { - const { issueActions } = props; +export const ProjectViewSpreadsheetLayout: React.FC = observer(() => { // router const router = useRouter(); const { viewId } = router.query; - const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); - return ( ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx index 4401eb839..346846def 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx @@ -1,10 +1,10 @@ import { useRef } from "react"; //types +import { observer } from "mobx-react"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; //components import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { HeaderColumn } from "./columns/header-column"; -import { observer } from "mobx-react"; interface Props { displayProperties: IIssueDisplayProperties; diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx index 98666d790..ea0e0f1c2 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx @@ -1,9 +1,9 @@ // ui import { LayersIcon } from "@plane/ui"; // types +import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; // constants -import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet"; // components import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; import { SpreadsheetHeaderColumn } from "./spreadsheet-header-column"; @@ -38,6 +38,7 @@ export const SpreadsheetHeader = (props: Props) => { {SPREADSHEET_PROPERTY_LIST.map((property) => ( React.ReactNode; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; canEditProperties: (projectId: string | undefined) => boolean; portalElement: React.MutableRefObject; containerRef: MutableRefObject; @@ -34,7 +33,7 @@ export const SpreadsheetTable = observer((props: Props) => { isEstimateEnabled, portalElement, quickActions, - handleIssues, + updateIssue, canEditProperties, containerRef, } = props; @@ -42,7 +41,7 @@ export const SpreadsheetTable = observer((props: Props) => { // states const isScrolled = useRef(false); - const handleScroll = () => { + const handleScroll = useCallback(() => { if (!containerRef.current) return; const scrollLeft = containerRef.current.scrollLeft; @@ -51,19 +50,19 @@ export const SpreadsheetTable = observer((props: Props) => { //The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly if (scrollLeft > 0 !== isScrolled.current) { - const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child"); + const firstColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child"); - for (let i = 0; i < firtColumns.length; i++) { + for (let i = 0; i < firstColumns.length; i++) { const shadow = i === 0 ? headerShadow : columnShadow; if (scrollLeft > 0) { - (firtColumns[i] as HTMLElement).style.boxShadow = shadow; + (firstColumns[i] as HTMLElement).style.boxShadow = shadow; } else { - (firtColumns[i] as HTMLElement).style.boxShadow = "none"; + (firstColumns[i] as HTMLElement).style.boxShadow = "none"; } } isScrolled.current = scrollLeft > 0; } - }; + }, [containerRef]); useEffect(() => { const currentContainerRef = containerRef.current; @@ -73,7 +72,7 @@ export const SpreadsheetTable = observer((props: Props) => { return () => { if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll); }; - }, []); + }, [handleScroll, containerRef]); const handleKeyBoardNavigation = useTableKeyboardNavigation(); @@ -95,7 +94,7 @@ export const SpreadsheetTable = observer((props: Props) => { canEditProperties={canEditProperties} nestingLevel={0} isEstimateEnabled={isEstimateEnabled} - handleIssues={handleIssues} + updateIssue={updateIssue} portalElement={portalElement} containerRef={containerRef} isScrolled={isScrolled} diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index e7b2bcee6..ed243d312 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -3,12 +3,11 @@ import { observer } from "mobx-react-lite"; // components import { Spinner } from "@plane/ui"; import { SpreadsheetQuickAddIssueForm } from "components/issues"; +import { useProject } from "hooks/store"; +import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { SpreadsheetTable } from "./spreadsheet-table"; // types -import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; -import { EIssueActions } from "../types"; //hooks -import { useProject } from "hooks/store"; type Props = { displayProperties: IIssueDisplayProperties; @@ -20,7 +19,7 @@ type Props = { customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null ) => React.ReactNode; - handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined; openIssuesListModal?: (() => void) | null; quickAddCallback?: ( workspaceSlug: string, @@ -41,7 +40,7 @@ export const SpreadsheetView: React.FC = observer((props) => { handleDisplayFilterUpdate, issueIds, quickActions, - handleIssues, + updateIssue, quickAddCallback, viewId, canEditProperties, @@ -75,7 +74,7 @@ export const SpreadsheetView: React.FC = observer((props) => { isEstimateEnabled={isEstimateEnabled} portalElement={portalRef} quickActions={quickActions} - handleIssues={handleIssues} + updateIssue={updateIssue} canEditProperties={canEditProperties} containerRef={containerRef} /> diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 0c3367dc1..ffe979a56 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -1,24 +1,39 @@ -import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui"; -import { EIssueListRow, ISSUE_PRIORITIES } from "constants/issue"; -import { renderEmoji } from "helpers/emoji.helper"; +import { ContrastIcon } from "lucide-react"; +import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; +// components +import { ProjectLogo } from "components/project"; +// stores +import { ISSUE_PRIORITIES } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; +import { ICycleStore } from "store/cycle.store"; +import { ILabelStore } from "store/label.store"; import { IMemberRootStore } from "store/member"; +import { IModuleStore } from "store/module.store"; import { IProjectStore } from "store/project/project.store"; import { IStateStore } from "store/state.store"; -import { GroupByColumnTypes, IGroupByColumn, IIssueListRow, TGroupedIssues, TUnGroupedIssues } from "@plane/types"; -import { STATE_GROUPS } from "constants/state"; -import { ILabelStore } from "store/label.store"; +// helpers +// constants +// types +import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types"; export const getGroupByColumns = ( groupBy: GroupByColumnTypes | null, project: IProjectStore, + cycle: ICycleStore, + module: IModuleStore, label: ILabelStore, projectState: IStateStore, member: IMemberRootStore, - includeNone?: boolean + includeNone?: boolean, + isWorkspaceLevel?: boolean ): IGroupByColumn[] | undefined => { switch (groupBy) { case "project": return getProjectColumns(project); + case "cycle": + return getCycleColumns(project, cycle); + case "module": + return getModuleColumns(project, module); case "state": return getStateColumns(projectState); case "state_detail.group": @@ -26,7 +41,7 @@ export const getGroupByColumns = ( case "priority": return getPriorityColumns(); case "labels": - return getLabelsColumns(label) as any; + return getLabelsColumns(label, isWorkspaceLevel) as any; case "assignees": return getAssigneeColumns(member) as any; case "created_by": @@ -49,12 +64,78 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined return { id: project.id, name: project.name, - icon:
{renderEmoji(project.emoji || "")}
, + icon: ( +
+ +
+ ), payload: { project_id: project.id }, }; }) as any; }; +const getCycleColumns = (projectStore: IProjectStore, cycleStore: ICycleStore): IGroupByColumn[] | undefined => { + const { currentProjectDetails } = projectStore; + const { getProjectCycleIds, getCycleById } = cycleStore; + + if (!currentProjectDetails || !currentProjectDetails?.id) return; + + const cycleIds = currentProjectDetails?.id ? getProjectCycleIds(currentProjectDetails?.id) : undefined; + if (!cycleIds) return; + + const cycles = []; + + cycleIds.map((cycleId) => { + const cycle = getCycleById(cycleId); + if (cycle) { + const cycleStatus = cycle.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft"; + cycles.push({ + id: cycle.id, + name: cycle.name, + icon: , + payload: { cycle_id: cycle.id }, + }); + } + }); + cycles.push({ + id: "None", + name: "None", + icon: , + }); + + return cycles as any; +}; + +const getModuleColumns = (projectStore: IProjectStore, moduleStore: IModuleStore): IGroupByColumn[] | undefined => { + const { currentProjectDetails } = projectStore; + const { getProjectModuleIds, getModuleById } = moduleStore; + + if (!currentProjectDetails || !currentProjectDetails?.id) return; + + const moduleIds = currentProjectDetails?.id ? getProjectModuleIds(currentProjectDetails?.id) : undefined; + if (!moduleIds) return; + + const modules = []; + + moduleIds.map((moduleId) => { + const moduleInfo = getModuleById(moduleId); + if (moduleInfo) + modules.push({ + id: moduleInfo.id, + name: moduleInfo.name, + icon: , + payload: { module_ids: [moduleInfo.id] }, + }); + }) as any; + modules.push({ + id: "None", + name: "None", + icon: , + }); + + return modules as any; +}; + const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => { const { projectStates } = projectState; if (!projectStates) return; @@ -63,7 +144,7 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine id: state.id, name: state.name, icon: ( -
+
), @@ -78,7 +159,7 @@ const getStateGroupColumns = () => { id: stateGroup.key, name: stateGroup.label, icon: ( -
+
), @@ -97,18 +178,19 @@ const getPriorityColumns = () => { })); }; -const getLabelsColumns = (label: ILabelStore) => { - const { projectLabels } = label; +const getLabelsColumns = (label: ILabelStore, isWorkspaceLevel: boolean = false) => { + const { workspaceLabels, projectLabels } = label; - if (!projectLabels) return; - - const labels = [...projectLabels, { id: "None", name: "None", color: "#666" }]; + const labels = [ + ...(isWorkspaceLevel ? workspaceLabels || [] : projectLabels || []), + { id: "None", name: "None", color: "#666" }, + ]; return labels.map((label) => ({ id: label.id, name: label.name, icon: ( -
+
), payload: label?.id === "None" ? {} : { label_ids: [label.id] }, })); diff --git a/web/components/issues/issue-modal/draft-issue-layout.tsx b/web/components/issues/issue-modal/draft-issue-layout.tsx index 1f9935c98..785ccb0bb 100644 --- a/web/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/components/issues/issue-modal/draft-issue-layout.tsx @@ -1,14 +1,15 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import useToast from "hooks/use-toast"; +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { ConfirmIssueDiscard } from "components/issues"; +import { IssueFormRoot } from "components/issues/issue-modal/form"; import { useEventTracker } from "hooks/store"; // services import { IssueDraftService } from "services/issue"; +// ui // components -import { IssueFormRoot } from "components/issues/issue-modal/form"; -import { ConfirmIssueDiscard } from "components/issues"; // types import type { TIssue } from "@plane/types"; @@ -43,8 +44,6 @@ export const DraftIssueLayout: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { captureIssueEvent } = useEventTracker(); @@ -61,8 +60,8 @@ export const DraftIssueLayout: React.FC = observer((props) => { await issueDraftService .createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload) .then((res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Draft Issue created successfully.", }); @@ -76,8 +75,8 @@ export const DraftIssueLayout: React.FC = observer((props) => { onClose(false); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be created. Please try again.", }); diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index e2e4e784e..dc1f42198 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -1,21 +1,13 @@ import React, { FC, useState, useRef, useEffect, Fragment } from "react"; -import { useRouter } from "next/router"; +import { RichTextEditorWithRef } from "@plane/rich-text-editor"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { LayoutPanelTop, Sparkle, X } from "lucide-react"; // editor -import { RichTextEditorWithRef } from "@plane/rich-text-editor"; // hooks -import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; -// services -import { AIService } from "services/ai.service"; -import { FileService } from "services/file.service"; -// components +import { Button, CustomMenu, Input, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { GptAssistantPopover } from "components/core"; -import { ParentIssuesListModal } from "components/issues"; -import { IssueLabelSelect } from "components/issues/select"; -import { CreateLabelModal } from "components/labels"; import { CycleDropdown, DateDropdown, @@ -26,10 +18,18 @@ import { MemberDropdown, StateDropdown, } from "components/dropdowns"; -// ui -import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui"; -// helpers +import { ParentIssuesListModal } from "components/issues"; +import { IssueLabelSelect } from "components/issues/select"; +import { CreateLabelModal } from "components/labels"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "hooks/store"; +// services +import { AIService } from "services/ai.service"; +import { FileService } from "services/file.service"; +// components +// ui +// helpers +import { getChangedIssuefields } from "helpers/issue.helper"; // types import type { TIssue, ISearchIssueResponse } from "@plane/types"; @@ -125,11 +125,9 @@ export const IssueFormRoot: FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); - // toast alert - const { setToastAlert } = useToast(); // form info const { - formState: { errors, isDirty, isSubmitting }, + formState: { errors, isDirty, isSubmitting, dirtyFields }, handleSubmit, reset, watch, @@ -169,7 +167,15 @@ export const IssueFormRoot: FC = observer((props) => { const issueName = watch("name"); const handleFormSubmit = async (formData: Partial, is_draft_issue = false) => { - await onSubmit(formData, is_draft_issue); + const submitData = !data?.id + ? formData + : { + ...getChangedIssuefields(formData, dirtyFields as { [key: string]: boolean | undefined }), + project_id: getValues("project_id"), + id: data.id, + description_html: formData.description_html ?? "

", + }; + await onSubmit(submitData, is_draft_issue); setGptAssistantModal(false); @@ -183,8 +189,7 @@ export const IssueFormRoot: FC = observer((props) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - setValue("description_html", `${watch("description_html")}

${response}

`); - editorRef.current?.setEditorValue(`${watch("description_html")}`); + editorRef.current?.setEditorValueAtCursorPosition(response); }; const handleAutoGenerateDescription = async () => { @@ -199,8 +204,8 @@ export const IssueFormRoot: FC = observer((props) => { }) .then((res) => { if (res.response === "") - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue title isn't informative enough to generate the description. Please try with a different title.", @@ -211,14 +216,14 @@ export const IssueFormRoot: FC = observer((props) => { const error = err?.data?.error; if (err.status === 429) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "You have reached the maximum number of requests of 50 requests per month per user.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error || "Some error occurred. Please try again.", }); @@ -363,14 +368,14 @@ export const IssueFormRoot: FC = observer((props) => { ref={ref} hasError={Boolean(errors.name)} placeholder="Issue Title" - className="resize-none text-xl w-full" + className="w-full resize-none text-xl" tabIndex={getTabIndex("name")} /> )} />
{data?.description_html === undefined ? ( - +
@@ -384,18 +389,18 @@ export const IssueFormRoot: FC = observer((props) => {
-
+
) : ( -
+
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && ( -
- - ); + +
+ + ); }; diff --git a/web/components/issues/parent-issues-list-modal.tsx b/web/components/issues/parent-issues-list-modal.tsx index c8520562e..f5b804e74 100644 --- a/web/components/issues/parent-issues-list-modal.tsx +++ b/web/components/issues/parent-issues-list-modal.tsx @@ -1,17 +1,15 @@ import React, { useEffect, useState } from "react"; - import { useRouter } from "next/router"; - // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // services +import { Rocket, Search } from "lucide-react"; +import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; +import useDebounce from "hooks/use-debounce"; import { ProjectService } from "services/project"; // hooks -import useDebounce from "hooks/use-debounce"; // ui -import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // icons -import { Rocket, Search } from "lucide-react"; // types import { ISearchIssueResponse } from "@plane/types"; @@ -136,7 +134,10 @@ export const ParentIssuesListModal: React.FC = ({
- + {searchTerm !== "" && (
Search results for{" "} diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx index 8db7fd0ac..b47551bc6 100644 --- a/web/components/issues/peek-overview/header.tsx +++ b/web/components/issues/peek-overview/header.tsx @@ -1,20 +1,29 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react"; // ui -import { ArchiveIcon, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Tooltip } from "@plane/ui"; -// helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -// hooks -import useToast from "hooks/use-toast"; -// store hooks -import { useIssueDetail, useProjectState, useUser } from "hooks/store"; -// helpers -import { cn } from "helpers/common.helper"; +import { + ArchiveIcon, + CenterPanelIcon, + CustomSelect, + FullScreenPanelIcon, + SidePanelIcon, + Tooltip, + TOAST_TYPE, + setToast, +} from "@plane/ui"; // components import { IssueSubscription, IssueUpdateStatus } from "components/issues"; import { STATE_GROUPS } from "constants/state"; +// helpers +import { cn } from "helpers/common.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +// store hooks +import { useIssueDetail, useProjectState, useUser } from "hooks/store"; +// helpers +// components +// helpers export type TPeekModes = "side-peek" | "modal" | "full-screen"; @@ -74,8 +83,6 @@ export const IssuePeekOverviewHeader: FC = observer((pr issue: { getIssueById }, } = useIssueDetail(); const { getStateById } = useProjectState(); - // hooks - const { setToastAlert } = useToast(); // derived values const issueDetails = getIssueById(issueId); const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined; @@ -87,8 +94,8 @@ export const IssuePeekOverviewHeader: FC = observer((pr e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(issueLink).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Issue link copied to clipboard.", }); diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 7f540874c..59b1c1609 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -1,14 +1,14 @@ import { FC, useEffect } from "react"; import { observer } from "mobx-react"; // store hooks +import { TIssueOperations } from "components/issues"; import { useIssueDetail, useProject, useUser } from "hooks/store"; // hooks import useReloadConfirmations from "hooks/use-reload-confirmation"; // components -import { TIssueOperations } from "components/issues"; +import { IssueDescriptionInput } from "../description-input"; import { IssueReaction } from "../issue-detail/reactions"; import { IssueTitleInput } from "../title-input"; -import { IssueDescriptionInput } from "../description-input"; interface IPeekOverviewIssueDetails { workspaceSlug: string; diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 2f5a02c11..8ae021b86 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -12,9 +12,9 @@ import { CalendarCheck2, } from "lucide-react"; // hooks -import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // ui icons import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui"; +import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; import { IssueLinkRoot, IssueCycleSelect, @@ -24,12 +24,12 @@ import { TIssueOperations, IssueRelationSelect, } from "components/issues"; -import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; // components +import { cn } from "helpers/common.helper"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // helpers -import { cn } from "helpers/common.helper"; import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; interface IPeekOverviewProperties { workspaceSlug: string; diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index 466bffee7..37cd8f375 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -1,17 +1,19 @@ import { FC, useEffect, useState, useMemo } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import useToast from "hooks/use-toast"; +import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; +import { IssueView } from "components/issues"; +// ui +// components +import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "constants/event-tracker"; +import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; // components -import { IssueView } from "components/issues"; // types import { TIssue } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EIssuesStoreType } from "constants/issue"; -import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "constants/event-tracker"; interface IIssuePeekOverview { is_archived?: boolean; @@ -20,13 +22,7 @@ interface IIssuePeekOverview { export type TIssuePeekOperations = { fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - update: ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast?: boolean - ) => Promise; + update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archive: (workspaceSlug: string, projectId: string, issueId: string) => Promise; restore: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -49,8 +45,6 @@ export type TIssuePeekOperations = { export const IssuePeekOverview: FC = observer((props) => { const { is_archived = false, is_draft = false } = props; - // hooks - const { setToastAlert } = useToast(); // router const router = useRouter(); const { @@ -86,49 +80,38 @@ export const IssuePeekOverview: FC = observer((props) => { console.error("Error fetching the parent issue"); } }, - update: async ( - workspaceSlug: string, - projectId: string, - issueId: string, - data: Partial, - showToast: boolean = true - ) => { - try { - await updateIssue(workspaceSlug, projectId, issueId, data); - if (showToast) - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", + update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + await updateIssue(workspaceSlug, projectId, issueId, data) + .then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" }, + updates: { + changed_property: Object.keys(data).join(","), + change_details: Object.values(data).join(","), + }, + path: router.asPath, + }); + }) + .catch(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { state: "FAILED", element: "Issue peek-overview" }, + path: router.asPath, + }); + setToast({ + title: "Issue update failed", + type: TOAST_TYPE.ERROR, + message: "Issue update failed", }); - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" }, - updates: { - changed_property: Object.keys(data).join(","), - change_details: Object.values(data).join(","), - }, - path: router.asPath, }); - } catch (error) { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { state: "FAILED", element: "Issue peek-overview" }, - path: router.asPath, - }); - setToastAlert({ - title: "Issue update failed", - type: "error", - message: "Issue update failed", - }); - } }, remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { removeIssue(workspaceSlug, projectId, issueId); - setToastAlert({ + setToast({ title: "Issue deleted successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Issue deleted successfully", }); captureIssueEvent({ @@ -137,9 +120,9 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ + setToast({ title: "Issue delete failed", - type: "error", + type: TOAST_TYPE.ERROR, message: "Issue delete failed", }); captureIssueEvent({ @@ -152,8 +135,8 @@ export const IssuePeekOverview: FC = observer((props) => { archive: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await archiveIssue(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue archived successfully.", }); @@ -163,8 +146,8 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be archived. Please try again.", }); @@ -178,8 +161,8 @@ export const IssuePeekOverview: FC = observer((props) => { restore: async (workspaceSlug: string, projectId: string, issueId: string) => { try { await restoreIssue(workspaceSlug, projectId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Issue restored successfully.", }); @@ -189,8 +172,8 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Issue could not be restored. Please try again.", }); @@ -203,12 +186,19 @@ export const IssuePeekOverview: FC = observer((props) => { }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { - await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); - setToastAlert({ - title: "Cycle added to issue successfully", - type: "success", - message: "Issue added to issue successfully", + const addToCyclePromise = addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); + setPromiseToast(addToCyclePromise, { + loading: "Adding cycle to issue...", + success: { + title: "Success!", + message: () => "Cycle added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle add to issue failed", + }, }); + await addToCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" }, @@ -228,24 +218,26 @@ export const IssuePeekOverview: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Cycle add to issue failed", - type: "error", - message: "Cycle add to issue failed", - }); } }, removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { try { - const response = await removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); - setToastAlert({ - title: "Cycle removed from issue successfully", - type: "success", - message: "Cycle removed from issue successfully", + const removeFromCyclePromise = removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); + setPromiseToast(removeFromCyclePromise, { + loading: "Removing cycle from issue...", + success: { + title: "Success!", + message: () => "Cycle removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Cycle remove from issue failed", + }, }); + await removeFromCyclePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, - payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, + payload: { issueId, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", change_details: "", @@ -253,11 +245,6 @@ export const IssuePeekOverview: FC = observer((props) => { path: router.asPath, }); } catch (error) { - setToastAlert({ - title: "Cycle remove from issue failed", - type: "error", - message: "Cycle remove from issue failed", - }); captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue peek-overview" }, @@ -271,12 +258,19 @@ export const IssuePeekOverview: FC = observer((props) => { }, addModulesToIssue: async (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => { try { - const response = await addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - title: "Module added to issue successfully", - type: "success", - message: "Module added to issue successfully", + const addToModulePromise = addModulesToIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(addToModulePromise, { + loading: "Adding module to issue...", + success: { + title: "Success!", + message: () => "Module added to issue successfully", + }, + error: { + title: "Error!", + message: () => "Module add to issue failed", + }, }); + const response = await addToModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, @@ -296,21 +290,23 @@ export const IssuePeekOverview: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module add to issue failed", - type: "error", - message: "Module add to issue failed", - }); } }, removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { try { - await removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); - setToastAlert({ - title: "Module removed from issue successfully", - type: "success", - message: "Module removed from issue successfully", + const removeFromModulePromise = removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); + setPromiseToast(removeFromModulePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Module remove from issue failed", + }, }); + await removeFromModulePromise; captureIssueEvent({ eventName: ISSUE_UPDATED, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, @@ -330,11 +326,6 @@ export const IssuePeekOverview: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - title: "Module remove from issue failed", - type: "error", - message: "Module remove from issue failed", - }); } }, removeModulesFromIssue: async ( @@ -343,20 +334,19 @@ export const IssuePeekOverview: FC = observer((props) => { issueId: string, moduleIds: string[] ) => { - try { - await removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); - setToastAlert({ - title: "Module removed from issue successfully", - type: "success", - message: "Module removed from issue successfully", - }); - } catch (error) { - setToastAlert({ - title: "Module remove from issue failed", - type: "error", - message: "Module remove from issue failed", - }); - } + const removeModulesFromIssuePromise = removeModulesFromIssue(workspaceSlug, projectId, issueId, moduleIds); + setPromiseToast(removeModulesFromIssuePromise, { + loading: "Removing module from issue...", + success: { + title: "Success!", + message: () => "Module removed from issue successfully", + }, + error: { + title: "Error!", + message: () => "Module remove from issue failed", + }, + }); + await removeModulesFromIssuePromise; }, }), [ @@ -372,7 +362,6 @@ export const IssuePeekOverview: FC = observer((props) => { addModulesToIssue, removeIssueFromModule, removeModulesFromIssue, - setToastAlert, captureIssueEvent, router.asPath, ] diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index cb2100ce0..c3ac1495a 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -1,13 +1,7 @@ import { FC, useRef, useState } from "react"; - import { observer } from "mobx-react-lite"; - -// hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import useKeypress from "hooks/use-keypress"; -import useToast from "hooks/use-toast"; -// store hooks -import { useIssueDetail } from "hooks/store"; +// ui +import { Spinner } from "@plane/ui"; // components import { DeleteIssueModal, @@ -18,9 +12,12 @@ import { TIssueOperations, ArchiveIssueModal, } from "components/issues"; +// hooks +import { useIssueDetail } from "hooks/store"; +import useKeypress from "hooks/use-keypress"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// store hooks import { IssueActivity } from "../issue-detail/issue-activity"; -// ui -import { Spinner } from "@plane/ui"; interface IIssueView { workspaceSlug: string; @@ -50,20 +47,20 @@ export const IssueView: FC = observer((props) => { issue: { getIssueById }, } = useIssueDetail(); const issue = getIssueById(issueId); - // hooks - const { alerts } = useToast(); // remove peek id const removeRoutePeekId = () => { setPeekIssue(undefined); }; useOutsideClickDetector(issuePeekOverviewRef, () => { - if (!isAnyModalOpen && (!alerts || alerts.length === 0)) { + if (!isAnyModalOpen) { removeRoutePeekId(); } }); const handleKeyDown = () => { - if (!isAnyModalOpen) { + const slashCommandDropdownElement = document.querySelector("#slash-command"); + const dropdownElement = document.activeElement?.tagName === "INPUT"; + if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement) { removeRoutePeekId(); const issueElement = document.getElementById(`issue-${issueId}`); if (issueElement) issueElement?.focus(); @@ -142,7 +139,7 @@ export const IssueView: FC = observer((props) => { disabled={disabled} /> {/* content */} -
+
{isLoading && !issue ? (
@@ -173,7 +170,7 @@ export const IssueView: FC = observer((props) => {
) : ( -
+
>; diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx index c6b87411d..5d7d19730 100644 --- a/web/components/issues/sub-issues/issue-list-item.tsx +++ b/web/components/issues/sub-issues/issue-list-item.tsx @@ -1,16 +1,16 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; // components +import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; +import { TIssue } from "@plane/types"; import { IssueList } from "./issues-list"; import { IssueProperty } from "./properties"; // ui -import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; // types -import { TIssue } from "@plane/types"; import { TSubIssueOperations } from "./root"; // import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; -import { useIssueDetail, useProject, useProjectState } from "hooks/store"; -import { observer } from "mobx-react-lite"; export interface ISubIssues { workspaceSlug: string; @@ -117,7 +117,7 @@ export const IssueListItem: React.FC = observer((props) => { onClick={() => handleIssuePeekOverview(issue)} className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" > - + {issue.name} diff --git a/web/components/issues/sub-issues/issues-list.tsx b/web/components/issues/sub-issues/issues-list.tsx index ad09938cb..cb1d66461 100644 --- a/web/components/issues/sub-issues/issues-list.tsx +++ b/web/components/issues/sub-issues/issues-list.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; // hooks import { useIssueDetail } from "hooks/store"; // components +import { TIssue } from "@plane/types"; import { IssueListItem } from "./issue-list-item"; // types -import { TIssue } from "@plane/types"; import { TSubIssueOperations } from "./root"; export interface IIssueList { diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx index 03c9d8902..f737b57e7 100644 --- a/web/components/issues/sub-issues/properties.tsx +++ b/web/components/issues/sub-issues/properties.tsx @@ -1,8 +1,8 @@ import React from "react"; // hooks +import { PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; import { useIssueDetail } from "hooks/store"; // components -import { PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; // types import { TSubIssueOperations } from "./root"; diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index 5e406116c..ed46a40f5 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -1,21 +1,20 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react"; // hooks -import { useEventTracker, useIssueDetail } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { copyTextToClipboard } from "helpers/string.helper"; +import { useEventTracker, useIssueDetail } from "hooks/store"; +// components +import { IUser, TIssue } from "@plane/types"; import { IssueList } from "./issues-list"; import { ProgressBar } from "./progressbar"; // ui -import { CustomMenu } from "@plane/ui"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IUser, TIssue } from "@plane/types"; export interface ISubIssuesRoot { workspaceSlug: string; @@ -46,8 +45,6 @@ export const SubIssuesRoot: FC = observer((props) => { const { workspaceSlug, projectId, parentIssueId, disabled = false } = props; // router const router = useRouter(); - // store hooks - const { setToastAlert } = useToast(); const { issue: { getIssueById }, subIssues: { subIssuesByIssueId, stateDistributionByIssueId, subIssueHelpersByIssueId, setSubIssueHelpers }, @@ -128,8 +125,8 @@ export const SubIssuesRoot: FC = observer((props) => { copyText: (text: string) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${text}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Issue link copied to clipboard.", }); @@ -139,8 +136,8 @@ export const SubIssuesRoot: FC = observer((props) => { try { await fetchSubIssues(workspaceSlug, projectId, parentIssueId); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error fetching sub-issues", message: "Error fetching sub-issues", }); @@ -149,14 +146,14 @@ export const SubIssuesRoot: FC = observer((props) => { addSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => { try { await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Sub-issues added successfully", message: "Sub-issues added successfully", }); } catch (error) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error adding sub-issue", message: "Error adding sub-issue", }); @@ -183,8 +180,8 @@ export const SubIssuesRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Sub-issue updated successfully", message: "Sub-issue updated successfully", }); @@ -199,8 +196,8 @@ export const SubIssuesRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error updating sub-issue", message: "Error updating sub-issue", }); @@ -210,8 +207,8 @@ export const SubIssuesRoot: FC = observer((props) => { try { setSubIssueHelpers(parentIssueId, "issue_loader", issueId); await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Sub-issue removed successfully", message: "Sub-issue removed successfully", }); @@ -235,8 +232,8 @@ export const SubIssuesRoot: FC = observer((props) => { }, path: router.asPath, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error removing sub-issue", message: "Error removing sub-issue", }); @@ -246,8 +243,8 @@ export const SubIssuesRoot: FC = observer((props) => { try { setSubIssueHelpers(parentIssueId, "issue_loader", issueId); await deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Issue deleted successfully", message: "Issue deleted successfully", }); @@ -263,15 +260,15 @@ export const SubIssuesRoot: FC = observer((props) => { payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, path: router.asPath, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error deleting issue", message: "Error deleting issue", }); } }, }), - [fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setToastAlert, setSubIssueHelpers] + [fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setSubIssueHelpers] ); const issue = getIssueById(parentIssueId); diff --git a/web/components/issues/title-input.tsx b/web/components/issues/title-input.tsx index 4a4057a6a..bb412b795 100644 --- a/web/components/issues/title-input.tsx +++ b/web/components/issues/title-input.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react"; // components import { TextArea } from "@plane/ui"; // types +import useDebounce from "hooks/use-debounce"; import { TIssueOperations } from "./issue-detail"; // hooks -import useDebounce from "hooks/use-debounce"; export type IssueTitleInputProps = { disabled?: boolean; @@ -32,7 +32,7 @@ export const IssueTitleInput: FC = observer((props) => { useEffect(() => { const textarea = document.querySelector("#title-input"); if (debouncedValue && debouncedValue !== value) { - issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }, false).finally(() => { + issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }).finally(() => { setIsSubmitting("saved"); if (textarea && !textarea.matches(":focus")) { const trimmedTitle = debouncedValue.trim(); diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx index e53e91147..ee0988741 100644 --- a/web/components/labels/create-label-modal.tsx +++ b/web/components/labels/create-label-modal.tsx @@ -1,19 +1,18 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Controller, useForm } from "react-hook-form"; +import { useRouter } from "next/router"; import { TwitterPicker } from "react-color"; +import { Controller, useForm } from "react-hook-form"; import { Dialog, Popover, Transition } from "@headlessui/react"; import { ChevronDown } from "lucide-react"; // hooks +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; import { useLabel } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; // types import type { IIssueLabel, IState } from "@plane/types"; // constants -import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; // types type Props = { @@ -64,8 +63,6 @@ export const CreateLabelModal: React.FC = observer((props) => { reset(defaultValues); }; - const { setToastAlert } = useToast(); - const onSubmit = async (formData: IIssueLabel) => { if (!workspaceSlug) return; @@ -75,9 +72,9 @@ export const CreateLabelModal: React.FC = observer((props) => { if (onSuccess) onSuccess(res); }) .catch((error) => { - setToastAlert({ + setToast({ title: "Oops!", - type: "error", + type: TOAST_TYPE.ERROR, message: error?.error ?? "Error while adding the label", }); reset(formData); diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index 2d2be046d..a29a334b6 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -1,18 +1,17 @@ import React, { forwardRef, useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { TwitterPicker } from "react-color"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { Popover, Transition } from "@headlessui/react"; +// ui +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; // hooks import { useLabel } from "hooks/store"; -import useToast from "hooks/use-toast"; -// ui -import { Button, Input } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; -// fetch-keys -import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; type Props = { labelForm: boolean; @@ -35,8 +34,6 @@ export const CreateUpdateLabelInline = observer( const { workspaceSlug, projectId } = router.query; // store hooks const { createLabel, updateLabel } = useLabel(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -65,9 +62,9 @@ export const CreateUpdateLabelInline = observer( reset(defaultValues); }) .catch((error) => { - setToastAlert({ + setToast({ title: "Oops!", - type: "error", + type: TOAST_TYPE.ERROR, message: error?.error ?? "Error while adding the label", }); reset(formData); @@ -77,15 +74,16 @@ export const CreateUpdateLabelInline = observer( const handleLabelUpdate: SubmitHandler = async (formData) => { if (!workspaceSlug || !projectId || isSubmitting) return; + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain await updateLabel(workspaceSlug.toString(), projectId.toString(), labelToUpdate?.id!, formData) .then(() => { reset(defaultValues); handleClose(); }) .catch((error) => { - setToastAlert({ + setToast({ title: "Oops!", - type: "error", + type: TOAST_TYPE.ERROR, message: error?.error ?? "Error while updating the label", }); reset(formData); diff --git a/web/components/labels/delete-label-modal.tsx b/web/components/labels/delete-label-modal.tsx index 64d15eb65..d5c269136 100644 --- a/web/components/labels/delete-label-modal.tsx +++ b/web/components/labels/delete-label-modal.tsx @@ -1,15 +1,13 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; // hooks +import { AlertTriangle } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { useLabel } from "hooks/store"; // icons -import { AlertTriangle } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; // types import type { IIssueLabel } from "@plane/types"; @@ -28,8 +26,6 @@ export const DeleteLabelModal: React.FC = observer((props) => { const { deleteLabel } = useLabel(); // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); - // hooks - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -49,8 +45,8 @@ export const DeleteLabelModal: React.FC = observer((props) => { setIsDeleteLoading(false); const error = err?.error || "Label could not be deleted. Please try again."; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error, }); diff --git a/web/components/labels/label-block/label-item-block.tsx b/web/components/labels/label-block/label-item-block.tsx index eca3bcaaf..2a797d0b6 100644 --- a/web/components/labels/label-block/label-item-block.tsx +++ b/web/components/labels/label-block/label-item-block.tsx @@ -1,12 +1,12 @@ import { useRef, useState } from "react"; -import { LucideIcon, X } from "lucide-react"; import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd"; +import { LucideIcon, X } from "lucide-react"; //ui import { CustomMenu } from "@plane/ui"; //types +import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { IIssueLabel } from "@plane/types"; //hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; //components import { DragHandle } from "./drag-handle"; import { LabelName } from "./label-name"; diff --git a/web/components/labels/project-setting-label-group.tsx b/web/components/labels/project-setting-label-group.tsx index 71d11dacb..6519e581e 100644 --- a/web/components/labels/project-setting-label-group.tsx +++ b/web/components/labels/project-setting-label-group.tsx @@ -1,12 +1,4 @@ import React, { Dispatch, SetStateAction, useState } from "react"; -import { Disclosure, Transition } from "@headlessui/react"; - -// store -import { observer } from "mobx-react-lite"; -// icons -import { ChevronDown, Pencil, Trash2 } from "lucide-react"; -// types -import { IIssueLabel } from "@plane/types"; import { Draggable, DraggableProvided, @@ -14,10 +6,18 @@ import { DraggableStateSnapshot, Droppable, } from "@hello-pangea/dnd"; -import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; -import { CreateUpdateLabelInline } from "./create-update-label-inline"; -import { ProjectSettingLabelItem } from "./project-setting-label-item"; +import { observer } from "mobx-react-lite"; +import { Disclosure, Transition } from "@headlessui/react"; + +// store +// icons +import { ChevronDown, Pencil, Trash2 } from "lucide-react"; +// types import useDraggableInPortal from "hooks/use-draggable-portal"; +import { IIssueLabel } from "@plane/types"; +import { CreateUpdateLabelInline } from "./create-update-label-inline"; +import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; +import { ProjectSettingLabelItem } from "./project-setting-label-item"; type Props = { label: IIssueLabel; @@ -107,7 +107,7 @@ export const ProjectSettingLabelGroup: React.FC = observer((props) => { customMenuItems={customMenuItems} dragHandleProps={dragHandleProps} handleLabelDelete={handleLabelDelete} - isLabelGroup={true} + isLabelGroup /> )} diff --git a/web/components/labels/project-setting-label-item.tsx b/web/components/labels/project-setting-label-item.tsx index ed72e4503..30e424064 100644 --- a/web/components/labels/project-setting-label-item.tsx +++ b/web/components/labels/project-setting-label-item.tsx @@ -1,14 +1,14 @@ import React, { Dispatch, SetStateAction, useState } from "react"; -import { useRouter } from "next/router"; import { DraggableProvidedDragHandleProps, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { useRouter } from "next/router"; import { X, Pencil } from "lucide-react"; // hooks import { useLabel } from "hooks/store"; // types import { IIssueLabel } from "@plane/types"; // components -import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; import { CreateUpdateLabelInline } from "./create-update-label-inline"; +import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; type Props = { label: IIssueLabel; diff --git a/web/components/labels/project-setting-label-list.tsx b/web/components/labels/project-setting-label-list.tsx index fcd84d70a..1e83167ae 100644 --- a/web/components/labels/project-setting-label-list.tsx +++ b/web/components/labels/project-setting-label-list.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef } from "react"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; +import { observer } from "mobx-react"; import { DragDropContext, Draggable, @@ -9,9 +9,8 @@ import { DropResult, Droppable, } from "@hello-pangea/dnd"; -import { useTheme } from "next-themes"; // hooks -import { useLabel, useUser } from "hooks/store"; +import { useLabel } from "hooks/store"; import useDraggableInPortal from "hooks/use-draggable-portal"; // components import { @@ -20,13 +19,13 @@ import { ProjectSettingLabelGroup, ProjectSettingLabelItem, } from "components/labels"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui import { Button, Loader } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; // constants -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; const LABELS_ROOT = "labels.root"; @@ -41,10 +40,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { currentUser } = useUser(); const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel(); // portal const renderDraggable = useDraggableInPortal(); @@ -54,10 +50,6 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { setLabelForm(true); }; - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["labels"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "labels", isLightMode); - const onDragEnd = (result: DropResult) => { const { combine, draggableId, destination, source } = result; @@ -76,16 +68,18 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { if (destination?.droppableId === LABELS_ROOT) parentLabel = null; if (result.reason == "DROP" && childLabel != parentLabel) { - updateLabelPosition( - workspaceSlug?.toString()!, - projectId?.toString()!, - childLabel, - parentLabel, - index, - prevParentLabel == parentLabel, - prevIndex - ); - return; + if (workspaceSlug && projectId) { + updateLabelPosition( + workspaceSlug?.toString(), + projectId?.toString(), + childLabel, + parentLabel, + index, + prevParentLabel == parentLabel, + prevIndex + ); + return; + } } }; @@ -104,7 +98,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
{showLabelForm && ( -
+
{ {projectLabels ? ( projectLabels.length === 0 && !showLabelForm ? (
- +
) : ( projectLabelsTree && ( diff --git a/web/components/modules/delete-module-modal.tsx b/web/components/modules/delete-module-modal.tsx index bf2e529b7..f4c5e536b 100644 --- a/web/components/modules/delete-module-modal.tsx +++ b/web/components/modules/delete-module-modal.tsx @@ -1,18 +1,17 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; // hooks -import { useEventTracker, useModule } from "hooks/store"; -import useToast from "hooks/use-toast"; -// ui -import { Button } from "@plane/ui"; -// icons import { AlertTriangle } from "lucide-react"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { MODULE_DELETED } from "constants/event-tracker"; +import { useEventTracker, useModule } from "hooks/store"; +// ui +// icons // types import type { IModule } from "@plane/types"; // constants -import { MODULE_DELETED } from "constants/event-tracker"; type Props = { data: IModule; @@ -30,8 +29,6 @@ export const DeleteModuleModal: React.FC = observer((props) => { // store hooks const { captureModuleEvent } = useEventTracker(); const { deleteModule } = useModule(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -47,8 +44,8 @@ export const DeleteModuleModal: React.FC = observer((props) => { .then(() => { if (moduleId || peekModule) router.push(`/${workspaceSlug}/projects/${data.project_id}/modules`); handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module deleted successfully.", }); @@ -58,8 +55,8 @@ export const DeleteModuleModal: React.FC = observer((props) => { }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Module could not be deleted. Please try again.", }); diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index 1dde2b85d..4af097591 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -1,10 +1,10 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; // components -import { ModuleStatusSelect } from "components/modules"; -import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "components/dropdowns"; -// ui import { Button, Input, TextArea } from "@plane/ui"; +import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "components/dropdowns"; +import { ModuleStatusSelect } from "components/modules"; +// ui // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types diff --git a/web/components/modules/gantt-chart/blocks.tsx b/web/components/modules/gantt-chart/blocks.tsx index ed6b48892..073283df4 100644 --- a/web/components/modules/gantt-chart/blocks.tsx +++ b/web/components/modules/gantt-chart/blocks.tsx @@ -1,13 +1,13 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // hooks -import { useApplication, useModule } from "hooks/store"; // ui import { Tooltip, ModuleStatusIcon } from "@plane/ui"; // helpers +import { MODULE_STATUS } from "constants/module"; import { renderFormattedDate } from "helpers/date-time.helper"; // constants -import { MODULE_STATUS } from "constants/module"; +import { useApplication, useModule } from "hooks/store"; type Props = { moduleId: string; diff --git a/web/components/modules/gantt-chart/modules-list-layout.tsx b/web/components/modules/gantt-chart/modules-list-layout.tsx index c6caacc92..0a9b433c5 100644 --- a/web/components/modules/gantt-chart/modules-list-layout.tsx +++ b/web/components/modules/gantt-chart/modules-list-layout.tsx @@ -1,10 +1,10 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // mobx store -import { useModule, useProject } from "hooks/store"; // components import { GanttChartRoot, IBlockUpdateData, ModuleGanttSidebar } from "components/gantt-chart"; import { ModuleGanttBlock } from "components/modules"; +import { useModule, useProject } from "hooks/store"; // types import { IModule } from "@plane/types"; diff --git a/web/components/modules/modal.tsx b/web/components/modules/modal.tsx index 47f331396..f83372eaa 100644 --- a/web/components/modules/modal.tsx +++ b/web/components/modules/modal.tsx @@ -2,15 +2,16 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; +// components +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { ModuleForm } from "components/modules"; +import { MODULE_CREATED, MODULE_UPDATED } from "constants/event-tracker"; // hooks import { useEventTracker, useModule, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui // components -import { ModuleForm } from "components/modules"; // types import type { IModule } from "@plane/types"; -// constants -import { MODULE_CREATED, MODULE_UPDATED } from "constants/event-tracker"; type Props = { isOpen: boolean; @@ -36,8 +37,6 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { const { captureModuleEvent } = useEventTracker(); const { workspaceProjectIds } = useProject(); const { createModule, updateModuleDetails } = useModule(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { reset(defaultValues); @@ -55,8 +54,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { await createModule(workspaceSlug.toString(), selectedProjectId, payload) .then((res) => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module created successfully.", }); @@ -66,8 +65,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Module could not be created. Please try again.", }); @@ -86,19 +85,19 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { .then((res) => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module updated successfully.", }); captureModuleEvent({ eventName: MODULE_UPDATED, - payload: { ...res, changed_properties: Object.keys(dirtyFields), state: "SUCCESS" }, + payload: { ...res, changed_properties: Object.keys(dirtyFields || {}), state: "SUCCESS" }, }); }) .catch((err) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Module could not be updated. Please try again.", }); @@ -109,7 +108,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { }); }; - const handleFormSubmit = async (formData: Partial, dirtyFields: any) => { + const handleFormSubmit = async (formData: Partial, dirtyFields: unknown) => { if (!workspaceSlug || !projectId) return; const payload: Partial = { diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 4dec3df6e..4873e009c 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -1,22 +1,21 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // hooks -import { useEventTracker, useMember, useModule, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components +import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; -// ui -import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip } from "@plane/ui"; -// helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; -// constants +import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +import { useEventTracker, useMember, useModule, useUser } from "hooks/store"; +// components +// ui +// helpers +// constants type Props = { moduleId: string; @@ -30,8 +29,6 @@ export const ModuleCardItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -48,21 +45,27 @@ export const ModuleCardItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { + const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then( + () => { captureEvent(MODULE_FAVORITED, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the module to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding module to favorites...", + success: { + title: "Success!", + message: () => "Module added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the module to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: React.MouseEvent) => { @@ -70,29 +73,37 @@ export const ModuleCardItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { - captureEvent(MODULE_UNFAVORITED, { - module_id: moduleId, - element: "Grid layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the module from favorites. Please try again.", - }); + const removeFromFavoritePromise = removeModuleFromFavorites( + workspaceSlug.toString(), + projectId.toString(), + moduleId + ).then(() => { + captureEvent(MODULE_UNFAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing module from favorites...", + success: { + title: "Success!", + message: () => "Module removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the module from favorites. Please try again.", + }, + }); }; const handleCopyText = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Module link copied to clipboard.", }); @@ -148,8 +159,8 @@ export const ModuleCardItem: React.FC = observer((props) => { ? !moduleTotalIssues || moduleTotalIssues === 0 ? "0 Issue" : moduleTotalIssues === moduleDetails.completed_issues - ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}` - : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues` + ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}` + : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues` : "0 Issue"; return ( @@ -254,7 +265,7 @@ export const ModuleCardItem: React.FC = observer((props) => { ))} - + {isEditingAllowed && ( <> diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 3d7468f24..7fe25b918 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -1,22 +1,30 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; // hooks -import { useModule, useUser, useEventTracker, useMember } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components +import { + Avatar, + AvatarGroup, + CircularProgressIndicator, + CustomMenu, + Tooltip, + TOAST_TYPE, + setToast, + setPromiseToast, +} from "@plane/ui"; import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; -// ui -import { Avatar, AvatarGroup, CircularProgressIndicator, CustomMenu, Tooltip } from "@plane/ui"; -// helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; -// constants +import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; -import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +import { useModule, useUser, useEventTracker, useMember } from "hooks/store"; +// components +// ui +// helpers +// constants type Props = { moduleId: string; @@ -30,8 +38,6 @@ export const ModuleListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -48,21 +54,27 @@ export const ModuleListItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { + const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then( + () => { captureEvent(MODULE_FAVORITED, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the module to favorites. Please try again.", - }); - }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding module to favorites...", + success: { + title: "Success!", + message: () => "Module added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the module to favorites. Please try again.", + }, + }); }; const handleRemoveFromFavorites = (e: React.MouseEvent) => { @@ -70,29 +82,37 @@ export const ModuleListItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { - captureEvent(MODULE_UNFAVORITED, { - module_id: moduleId, - element: "Grid layout", - state: "SUCCESS", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the module from favorites. Please try again.", - }); + const removeFromFavoritePromise = removeModuleFromFavorites( + workspaceSlug.toString(), + projectId.toString(), + moduleId + ).then(() => { + captureEvent(MODULE_UNFAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing module from favorites...", + success: { + title: "Success!", + message: () => "Module removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the module from favorites. Please try again.", + }, + }); }; const handleCopyText = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Module link copied to clipboard.", }); @@ -155,9 +175,9 @@ export const ModuleListItem: React.FC = observer((props) => { )} setDeleteModal(false)} /> -
-
-
+
+
+
@@ -182,10 +202,10 @@ export const ModuleListItem: React.FC = observer((props) => {
-
+
{moduleStatus && ( = observer((props) => {
-
+
{renderDate && ( @@ -206,7 +226,7 @@ export const ModuleListItem: React.FC = observer((props) => { )}
-
+
{moduleDetails.member_ids.length > 0 ? ( @@ -235,7 +255,7 @@ export const ModuleListItem: React.FC = observer((props) => { ))} - + {isEditingAllowed && ( <> diff --git a/web/components/modules/module-mobile-header.tsx b/web/components/modules/module-mobile-header.tsx index e9ed56a8d..4763639ed 100644 --- a/web/components/modules/module-mobile-header.tsx +++ b/web/components/modules/module-mobile-header.tsx @@ -1,162 +1,164 @@ -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; +import { useCallback, useState } from "react"; +import router from "next/router"; +import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; import { CustomMenu } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "constants/issue"; import { useIssues, useLabel, useMember, useModule, useProjectState } from "hooks/store"; -import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; -import router from "next/router"; -import { useCallback, useState } from "react"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const ModuleMobileHeader = () => { - const [analyticsModal, setAnalyticsModal] = useState(false); - const { getModuleById } = useModule(); - const layouts = [ - { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Kanban", icon: Kanban }, - { key: "calendar", title: "Calendar", icon: Calendar }, - ]; - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; - const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; + const [analyticsModal, setAnalyticsModal] = useState(false); + const { getModuleById } = useModule(); + const layouts = [ + { key: "list", title: "List", icon: List }, + { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "calendar", title: "Calendar", icon: Calendar }, + ]; + const { workspaceSlug, projectId, moduleId } = router.query as { + workspaceSlug: string; + projectId: string; + moduleId: string; + }; + const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.MODULE); - const activeLayout = issueFilters?.displayFilters?.layout; - const { projectStates } = useProjectState(); - const { projectLabels } = useLabel(); - const { - project: { projectMemberIds }, - } = useMember(); + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.MODULE); + const activeLayout = issueFilters?.displayFilters?.layout; + const { projectStates } = useProjectState(); + const { projectLabels } = useLabel(); + const { + project: { projectMemberIds }, + } = useMember(); - const handleLayoutChange = useCallback( - (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); + const handleLayoutChange = useCallback( + (layout: TIssueLayouts) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); + }, + [workspaceSlug, projectId, moduleId, updateFilters] + ); - const handleFiltersUpdate = useCallback( - (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!workspaceSlug || !projectId) return; + const newValues = issueFilters?.filters?.[key] ?? []; - if (Array.isArray(value)) { - value.forEach((val) => { - if (!newValues.includes(val)) newValues.push(val); - }); - } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); + }, + [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] + ); + + const handleDisplayFilters = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); + }, + [workspaceSlug, projectId, moduleId, updateFilters] + ); + + const handleDisplayProperties = useCallback( + (property: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); + }, + [workspaceSlug, projectId, moduleId, updateFilters] + ); + + return ( +
+ setAnalyticsModal(false)} + moduleDetails={moduleDetails ?? undefined} + /> +
+ Layout} + customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" + closeOnSelect + > + {layouts.map((layout, index) => ( + { + handleLayoutChange(ISSUE_LAYOUTS[index].key); + }} + className="flex items-center gap-2" + > + +
{layout.title}
+
+ ))} +
+
+ + Filters + + } - - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); - }, - [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); - - return ( -
- setAnalyticsModal(false)} - moduleDetails={moduleDetails ?? undefined} + > + -
- Layout} - customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" - closeOnSelect - > - {layouts.map((layout, index) => ( - { - handleLayoutChange(ISSUE_LAYOUTS[index].key); - }} - className="flex items-center gap-2" - > - -
{layout.title}
-
- ))} -
-
- - Filters - - - } - > - - -
-
- - Display - - - } - > - - -
- - -
+
- ); +
+ + Display + + + } + > + + +
+ + +
+
+ ); }; diff --git a/web/components/modules/module-peek-overview.tsx b/web/components/modules/module-peek-overview.tsx index 81614b61b..5590d0390 100644 --- a/web/components/modules/module-peek-overview.tsx +++ b/web/components/modules/module-peek-overview.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks import { useModule } from "hooks/store"; // components diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index bf12fde8b..78b4a6571 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,40 +1,28 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; +import { useRouter } from "next/router"; // hooks -import { useApplication, useEventTracker, useModule, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useModule } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // constants -import { EUserProjectRoles } from "constants/project"; -import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; export const ModulesListView: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, peekModule } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); + const { projectModuleIds, loader } = useModule(); const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", isLightMode); - - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - if (loader || !projectModuleIds) return ( <> @@ -88,22 +76,11 @@ export const ModulesListView: React.FC = observer(() => { ) : ( { + setTrackElement("Module empty state"); + commandPaletteStore.toggleCreateModuleModal(true); }} - primaryButton={{ - text: MODULE_EMPTY_STATE_DETAILS["modules"].primaryButton.text, - onClick: () => { - setTrackElement("Module empty state"); - commandPaletteStore.toggleCreateModuleModal(true); - }, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/modules/select/status.tsx b/web/components/modules/select/status.tsx index 33a634e9b..8efdcb472 100644 --- a/web/components/modules/select/status.tsx +++ b/web/components/modules/select/status.tsx @@ -5,9 +5,9 @@ import { Controller, FieldError, Control } from "react-hook-form"; // ui import { CustomSelect, DoubleCircleIcon, ModuleStatusIcon } from "@plane/ui"; // types +import { MODULE_STATUS } from "constants/module"; import type { IModule } from "@plane/types"; // constants -import { MODULE_STATUS } from "constants/module"; type Props = { control: Control; diff --git a/web/components/modules/sidebar-select/select-status.tsx b/web/components/modules/sidebar-select/select-status.tsx index b8c337fd4..4a203ee62 100644 --- a/web/components/modules/sidebar-select/select-status.tsx +++ b/web/components/modules/sidebar-select/select-status.tsx @@ -5,10 +5,10 @@ import { Control, Controller, UseFormWatch } from "react-hook-form"; // ui import { CustomSelect, DoubleCircleIcon } from "@plane/ui"; // types +import { MODULE_STATUS } from "constants/module"; import { IModule } from "@plane/types"; // common // constants -import { MODULE_STATUS } from "constants/module"; type Props = { control: Control, any>; diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index 53d7eff4c..c9f28cf98 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; import { @@ -14,25 +14,33 @@ import { Trash2, UserCircle2, } from "lucide-react"; -// hooks -import { useModule, useUser, useEventTracker } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { + CustomMenu, + Loader, + LayersIcon, + CustomSelect, + ModuleStatusIcon, + UserGroupIcon, + TOAST_TYPE, + setToast, +} from "@plane/ui"; // components import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; -import { DeleteModuleModal } from "components/modules"; import ProgressChart from "components/core/sidebar/progress-chart"; import { DateRangeDropdown, MemberDropdown } from "components/dropdowns"; -// ui -import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon, UserGroupIcon } from "@plane/ui"; +import { DeleteModuleModal } from "components/modules"; +// constant +import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; +import { MODULE_STATUS } from "constants/module"; +import { EUserProjectRoles } from "constants/project"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useModule, useUser, useEventTracker } from "hooks/store"; // types import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; -// constant -import { MODULE_STATUS } from "constants/module"; -import { EUserProjectRoles } from "constants/project"; -import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; const defaultValues: Partial = { lead_id: "", @@ -65,8 +73,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); const moduleDetails = getModuleById(moduleId); - const { setToastAlert } = useToast(); - const { reset, control } = useForm({ defaultValues, }); @@ -99,15 +105,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { module_id: moduleId, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Module link created", message: "Module link created successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -125,15 +131,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { module_id: moduleId, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Module link updated", message: "Module link updated successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -149,15 +155,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { module_id: moduleId, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Module link deleted", message: "Module link deleted successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -167,15 +173,15 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const handleCopyText = () => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Module link copied to clipboard", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred", }); @@ -187,8 +193,8 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { start_date: startDate ? renderFormattedPayloadDate(startDate) : null, target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Module updated successfully.", }); @@ -334,7 +340,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { Date range
-
+
= observer((props) => {
- {moduleDetails.description && ( - - {moduleDetails.description} - - )} -
@@ -384,7 +384,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { control={control} name="lead_id" render={({ field: { value } }) => ( -
+
{ @@ -408,7 +408,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { control={control} name="member_ids" render={({ field: { value } }) => ( -
+
{ @@ -429,7 +429,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { Issues
-
+
{issueCount}
diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index 03d849a82..0e4904a7e 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -1,23 +1,22 @@ import React, { useEffect, useRef } from "react"; import Image from "next/image"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; import { Menu } from "@headlessui/react"; -import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; -import { useEventTracker } from "hooks/store"; // icons -import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui"; -// constants -import { snoozeOptions } from "constants/notification"; -// helper -import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper"; -import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; -// type -import type { IUserNotification, NotificationType } from "@plane/types"; +import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react"; +// ui +import { ArchiveIcon, CustomMenu, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { ISSUE_OPENED, NOTIFICATIONS_READ, NOTIFICATION_ARCHIVED, NOTIFICATION_SNOOZED } from "constants/event-tracker"; +import { snoozeOptions } from "constants/notification"; +// helper +import { calculateTimeAgo, renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; +import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper"; +// hooks +import { useEventTracker } from "hooks/store"; +// type +import type { IUserNotification, NotificationType } from "@plane/types"; type NotificationCardProps = { selectedTab: NotificationType; @@ -50,8 +49,6 @@ export const NotificationCard: React.FC = (props) => { const { workspaceSlug } = router.query; // states const [showSnoozeOptions, setShowSnoozeOptions] = React.useState(false); - // toast alert - const { setToastAlert } = useToast(); // refs const snoozeRef = useRef(null); @@ -62,9 +59,9 @@ export const NotificationCard: React.FC = (props) => { icon: , onClick: () => { markNotificationReadStatusToggle(notification.id).then(() => { - setToastAlert({ + setToast({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -79,9 +76,9 @@ export const NotificationCard: React.FC = (props) => { ), onClick: () => { markNotificationArchivedStatus(notification.id).then(() => { - setToastAlert({ + setToast({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -94,9 +91,9 @@ export const NotificationCard: React.FC = (props) => { return; } markSnoozeNotification(notification.id, date).then(() => { - setToastAlert({ + setToast({ title: `Notification snoozed till ${renderFormattedDate(date)}`, - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }; @@ -179,12 +176,12 @@ export const NotificationCard: React.FC = (props) => { {notificationField === "comment" ? "commented" : notificationField === "archived_at" - ? notification.data.issue_activity.new_value === "restore" - ? "restored the issue" - : "archived the issue" - : notificationField === "None" - ? null - : replaceUnderscoreIfSnakeCase(notificationField)}{" "} + ? notification.data.issue_activity.new_value === "restore" + ? "restored the issue" + : "archived the issue" + : notificationField === "None" + ? null + : replaceUnderscoreIfSnakeCase(notificationField)}{" "} {!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""} {" "} @@ -218,7 +215,7 @@ export const NotificationCard: React.FC = (props) => { {notification.message}
)} -
+
{({ open }) => ( <> @@ -234,11 +231,11 @@ export const NotificationCard: React.FC = (props) => {
{moreOptions.map((item) => ( - + {({ close }) => (
@@ -330,9 +328,9 @@ export const NotificationCard: React.FC = (props) => { tab: selectedTab, state: "SUCCESS", }); - setToastAlert({ + setToast({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, @@ -352,15 +350,15 @@ export const NotificationCard: React.FC = (props) => { tab: selectedTab, state: "SUCCESS", }); - setToastAlert({ + setToast({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }, }, ].map((item) => ( - +
diff --git a/web/components/notifications/notification-popover.tsx b/web/components/notifications/notification-popover.tsx index a8f25762e..d7aa1b07d 100644 --- a/web/components/notifications/notification-popover.tsx +++ b/web/components/notifications/notification-popover.tsx @@ -1,20 +1,20 @@ import React, { Fragment } from "react"; +import { observer } from "mobx-react-lite"; import { Popover, Transition } from "@headlessui/react"; import { Bell } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; -import useUserNotification from "hooks/use-user-notifications"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// components +import { Tooltip } from "@plane/ui"; import { EmptyState } from "components/common"; import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "components/notifications"; -import { Tooltip } from "@plane/ui"; import { NotificationsLoader } from "components/ui"; +import { getNumberCount } from "helpers/string.helper"; +import { useApplication } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import useUserNotification from "hooks/use-user-notifications"; +// components // images import emptyNotification from "public/empty-state/notification.svg"; // helpers -import { getNumberCount } from "helpers/string.helper"; export const NotificationPopover = observer(() => { // states diff --git a/web/components/notifications/select-snooze-till-modal.tsx b/web/components/notifications/select-snooze-till-modal.tsx index f89b3a963..f65d51ba7 100644 --- a/web/components/notifications/select-snooze-till-modal.tsx +++ b/web/components/notifications/select-snooze-till-modal.tsx @@ -1,15 +1,13 @@ import { Fragment, FC } from "react"; import { useRouter } from "next/router"; import { useForm, Controller } from "react-hook-form"; -import { DateDropdown } from "components/dropdowns"; import { Transition, Dialog } from "@headlessui/react"; import { X } from "lucide-react"; +import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; +import { DateDropdown } from "components/dropdowns"; // constants import { allTimeIn30MinutesInterval12HoursFormat } from "constants/notification"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect } from "@plane/ui"; // types import type { IUserNotification } from "@plane/types"; @@ -41,8 +39,6 @@ export const SnoozeNotificationModal: FC = (props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const { formState: { isSubmitting }, reset, @@ -100,10 +96,10 @@ export const SnoozeNotificationModal: FC = (props) => { await handleSubmitSnooze(notification.id, dateTime).then(() => { handleClose(); onSuccess(); - setToastAlert({ + setToast({ title: "Notification snoozed", message: "Notification snoozed successfully", - type: "success", + type: TOAST_TYPE.SUCCESS, }); }); }; @@ -147,7 +143,7 @@ export const SnoozeNotificationModal: FC = (props) => { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - +
@@ -161,7 +157,7 @@ export const SnoozeNotificationModal: FC = (props) => {
-
+
Pick a date
= (props) => { onClick={() => { setValue("period", "AM"); }} - className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "AM" + className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${ + watch("period") === "AM" ? "bg-custom-primary-100/90 text-custom-primary-0" : "bg-custom-background-80" - }`} + }`} > AM
@@ -225,10 +222,11 @@ export const SnoozeNotificationModal: FC = (props) => { onClick={() => { setValue("period", "PM"); }} - className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${watch("period") === "PM" + className={`flex h-full w-1/2 cursor-pointer items-center justify-center text-center ${ + watch("period") === "PM" ? "bg-custom-primary-100/90 text-custom-primary-0" : "bg-custom-background-80" - }`} + }`} > PM
diff --git a/web/components/onboarding/invitations.tsx b/web/components/onboarding/invitations.tsx index c176ed580..2e94bb67e 100644 --- a/web/components/onboarding/invitations.tsx +++ b/web/components/onboarding/invitations.tsx @@ -1,23 +1,23 @@ import React, { useState } from "react"; import useSWR, { mutate } from "swr"; // hooks +import { CheckCircle2, Search } from "lucide-react"; +import { Button } from "@plane/ui"; +import { MEMBER_ACCEPTED } from "constants/event-tracker"; +import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; +import { ROLE } from "constants/workspace"; +import { truncateText } from "helpers/string.helper"; +import { getUserRole } from "helpers/user.helper"; import { useEventTracker, useUser, useWorkspace } from "hooks/store"; // components -import { Button } from "@plane/ui"; // helpers -import { truncateText } from "helpers/string.helper"; // services import { WorkspaceService } from "services/workspace.service"; // constants -import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; -import { ROLE } from "constants/workspace"; -import { MEMBER_ACCEPTED } from "constants/event-tracker"; // types import { IWorkspaceMemberInvitation } from "@plane/types"; // icons -import { CheckCircle2, Search } from "lucide-react"; import {} from "hooks/store/use-event-tracker"; -import { getUserRole } from "helpers/user.helper"; type Props = { handleNextStep: () => void; @@ -57,17 +57,18 @@ export const Invitations: React.FC = (props) => { }; const submitInvitations = async () => { - if (invitationsRespond.length <= 0) return; + const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]); + + if (invitationsRespond.length <= 0 && !invitation?.role) return; setIsJoiningWorkspaces(true); - const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]); await workspaceService .joinWorkspaces({ invitations: invitationsRespond }) .then(async () => { captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, - role: getUserRole(invitation?.role!), + role: getUserRole(invitation?.role as any), project_id: undefined, accepted_from: "App", state: "SUCCESS", @@ -83,7 +84,7 @@ export const Invitations: React.FC = (props) => { console.error(error); captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, - role: getUserRole(invitation?.role!), + role: getUserRole(invitation?.role as any), project_id: undefined, accepted_from: "App", state: "FAILED", diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index 561a428d6..d0540a5a4 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import Image from "next/image"; import { useTheme } from "next-themes"; -import { Listbox, Transition } from "@headlessui/react"; import { Control, Controller, @@ -13,30 +12,30 @@ import { useFieldArray, useForm, } from "react-hook-form"; +import { Listbox, Transition } from "@headlessui/react"; +// icons import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; -// services -import { WorkspaceService } from "services/workspace.service"; -// hooks -import useToast from "hooks/use-toast"; -import { useEventTracker } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components import { OnboardingStepIndicator } from "components/onboarding/step-indicator"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// types -import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; // constants -import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { MEMBER_INVITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; // helpers import { getUserRole } from "helpers/user.helper"; +// hooks +import { useEventTracker } from "hooks/store"; +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; // assets -import user1 from "public/users/user-1.png"; -import user2 from "public/users/user-2.png"; import userDark from "public/onboarding/user-dark.svg"; import userLight from "public/onboarding/user-light.svg"; +import user1 from "public/users/user-1.png"; +import user2 from "public/users/user-2.png"; +// services +import { WorkspaceService } from "services/workspace.service"; +// types +import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; type Props = { finishOnboarding: () => Promise; @@ -269,7 +268,6 @@ export const InviteMembers: React.FC = (props) => { const [isInvitationDisabled, setIsInvitationDisabled] = useState(true); - const { setToastAlert } = useToast(); const { resolvedTheme } = useTheme(); // store hooks const { captureEvent } = useEventTracker(); @@ -322,8 +320,8 @@ export const InviteMembers: React.FC = (props) => { state: "SUCCESS", element: "Onboarding", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Invitations sent successfully.", }); @@ -336,8 +334,8 @@ export const InviteMembers: React.FC = (props) => { state: "FAILED", element: "Onboarding", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error, }); @@ -370,8 +368,8 @@ export const InviteMembers: React.FC = (props) => { >

Members

- {Array.from({ length: 4 }).map(() => ( -
+ {Array.from({ length: 4 }).map((i, index) => ( +
user
diff --git a/web/components/onboarding/join-workspaces.tsx b/web/components/onboarding/join-workspaces.tsx index 08ffab379..e59db31c7 100644 --- a/web/components/onboarding/join-workspaces.tsx +++ b/web/components/onboarding/join-workspaces.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { Controller, useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; // hooks +import { Invitations, OnboardingSidebar, OnboardingStepIndicator, Workspace } from "components/onboarding"; import { useUser } from "hooks/store"; // components -import { Invitations, OnboardingSidebar, OnboardingStepIndicator, Workspace } from "components/onboarding"; // types import { IWorkspace, TOnboardingSteps } from "@plane/types"; diff --git a/web/components/onboarding/onboarding-sidebar.tsx b/web/components/onboarding/onboarding-sidebar.tsx index af0da75ca..42ec102cb 100644 --- a/web/components/onboarding/onboarding-sidebar.tsx +++ b/web/components/onboarding/onboarding-sidebar.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; -import { useTheme } from "next-themes"; import Image from "next/image"; +import { useTheme } from "next-themes"; import { Control, Controller, UseFormSetValue, UseFormWatch } from "react-hook-form"; import { BarChart2, @@ -20,9 +20,9 @@ import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui"; // hooks import { useUser, useWorkspace } from "hooks/store"; // types +import projectEmoji from "public/emoji/project-emoji.svg"; import { IWorkspace } from "@plane/types"; // assets -import projectEmoji from "public/emoji/project-emoji.svg"; const workspaceLinks = [ { @@ -86,8 +86,9 @@ type Props = { watch?: UseFormWatch; userFullName?: string; }; -var timer: number = 0; -var lastWorkspaceName: string = ""; + +let timer: number = 0; +let lastWorkspaceName: string = ""; export const OnboardingSidebar: React.FC = (props) => { const { workspaceName, showProject, control, setValue, watch, userFullName } = props; diff --git a/web/components/onboarding/switch-delete-account-modal.tsx b/web/components/onboarding/switch-delete-account-modal.tsx index 66b98fb23..c84911220 100644 --- a/web/components/onboarding/switch-delete-account-modal.tsx +++ b/web/components/onboarding/switch-delete-account-modal.tsx @@ -1,12 +1,13 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; import { useTheme } from "next-themes"; +import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; // hooks +import { TOAST_TYPE, setToast } from "@plane/ui"; import { useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui type Props = { isOpen: boolean; @@ -25,8 +26,6 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { const { resolvedTheme, setTheme } = useTheme(); - const { setToastAlert } = useToast(); - const handleClose = () => { setSwitchingAccount(false); setIsDeactivating(false); @@ -44,8 +43,8 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { handleClose(); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) @@ -58,8 +57,8 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { await deactivateAccount() .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Account deleted successfully.", }); @@ -69,8 +68,8 @@ export const SwitchOrDeleteAccountModal: React.FC = (props) => { handleClose(); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error, }) diff --git a/web/components/onboarding/tour/root.tsx b/web/components/onboarding/tour/root.tsx index c09a2a94c..4c44f8c62 100644 --- a/web/components/onboarding/tour/root.tsx +++ b/web/components/onboarding/tour/root.tsx @@ -1,22 +1,22 @@ import { useState } from "react"; -import Image from "next/image"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; import { X } from "lucide-react"; // hooks +import { Button } from "@plane/ui"; +import { TourSidebar } from "components/onboarding"; +import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker"; import { useApplication, useEventTracker, useUser } from "hooks/store"; // components -import { TourSidebar } from "components/onboarding"; // ui -import { Button } from "@plane/ui"; // assets -import PlaneWhiteLogo from "public/plane-logos/white-horizontal.svg"; -import IssuesTour from "public/onboarding/issues.webp"; import CyclesTour from "public/onboarding/cycles.webp"; +import IssuesTour from "public/onboarding/issues.webp"; import ModulesTour from "public/onboarding/modules.webp"; -import ViewsTour from "public/onboarding/views.webp"; import PagesTour from "public/onboarding/pages.webp"; +import ViewsTour from "public/onboarding/views.webp"; +import PlaneWhiteLogo from "public/plane-logos/white-horizontal.svg"; // constants -import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker"; type Props = { onComplete: () => void; diff --git a/web/components/onboarding/tour/sidebar.tsx b/web/components/onboarding/tour/sidebar.tsx index 350bd638a..535002493 100644 --- a/web/components/onboarding/tour/sidebar.tsx +++ b/web/components/onboarding/tour/sidebar.tsx @@ -1,6 +1,6 @@ // icons -import { ContrastIcon, DiceIcon, LayersIcon, PhotoFilterIcon } from "@plane/ui"; import { FileText } from "lucide-react"; +import { ContrastIcon, DiceIcon, LayersIcon, PhotoFilterIcon } from "@plane/ui"; // types import { TTourSteps } from "./root"; diff --git a/web/components/onboarding/user-details.tsx b/web/components/onboarding/user-details.tsx index a29df3c94..820f08da6 100644 --- a/web/components/onboarding/user-details.tsx +++ b/web/components/onboarding/user-details.tsx @@ -1,21 +1,22 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Image from "next/image"; import { Controller, useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; import { Camera, User2 } from "lucide-react"; +import { Button, Input } from "@plane/ui"; +// components +import { UserImageUploadModal } from "components/core"; +import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding"; +// constants +import { USER_DETAILS } from "constants/event-tracker"; // hooks import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -// components -import { Button, Input } from "@plane/ui"; -import { OnboardingSidebar, OnboardingStepIndicator } from "components/onboarding"; -import { UserImageUploadModal } from "components/core"; -// types -import { IUser } from "@plane/types"; -// services -import { FileService } from "services/file.service"; // assets import IssuesSvg from "public/onboarding/onboarding-issues.webp"; -import { USER_DETAILS } from "constants/event-tracker"; +// services +import { FileService } from "services/file.service"; +// types +import { IUser } from "@plane/types"; const defaultValues: Partial = { first_name: "", @@ -183,7 +184,7 @@ export const UserDetails: React.FC = observer((props) => { name="first_name" type="text" value={value} - autoFocus={true} + autoFocus onChange={(event) => { setUserName(event.target.value); onChange(event); @@ -220,6 +221,7 @@ export const UserDetails: React.FC = observer((props) => {
{USE_CASES.map((useCase) => (
) => Promise; @@ -35,8 +33,6 @@ export const Workspace: React.FC = (props) => { const { updateCurrentUser } = useUser(); const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace(); const { captureWorkspaceEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); const handleCreateWorkspace = async (formData: IWorkspace) => { if (isSubmitting) return; @@ -49,8 +45,8 @@ export const Workspace: React.FC = (props) => { await createWorkspace(formData) .then(async (res) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace created successfully.", }); @@ -75,8 +71,8 @@ export const Workspace: React.FC = (props) => { element: "Onboarding", }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Workspace could not be created. Please try again.", }); @@ -84,8 +80,8 @@ export const Workspace: React.FC = (props) => { } else setSlugError(true); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred while creating workspace. Please try again.", }) diff --git a/web/components/page-views/signin.tsx b/web/components/page-views/signin.tsx index 2f1d62f84..7929a5e37 100644 --- a/web/components/page-views/signin.tsx +++ b/web/components/page-views/signin.tsx @@ -2,13 +2,13 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import Image from "next/image"; // hooks +import { Spinner } from "@plane/ui"; +import { SignInRoot } from "components/account"; +import { PageHead } from "components/core"; import { useApplication, useUser } from "hooks/store"; import useSignInRedirection from "hooks/use-sign-in-redirection"; // components -import { SignInRoot } from "components/account"; -import { PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index 0d5d4115a..f910625ca 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -1,41 +1,30 @@ import { useEffect } from "react"; -import { useTheme } from "next-themes"; import { observer } from "mobx-react-lite"; // hooks import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; // components +import { DashboardWidgets } from "components/dashboard"; +import { EmptyState } from "components/empty-state"; +import { IssuePeekOverview } from "components/issues"; import { TourRoot } from "components/onboarding"; import { UserGreetingsView } from "components/user"; -import { IssuePeekOverview } from "components/issues"; -import { DashboardWidgets } from "components/dashboard"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui import { Spinner } from "@plane/ui"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker"; -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; export const WorkspaceDashboardView = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { captureEvent, setTrackElement } = useEventTracker(); const { commandPalette: { toggleCreateProjectModal }, router: { workspaceSlug }, } = useApplication(); - const { - currentUser, - updateTourCompleted, - membership: { currentWorkspaceRole }, - } = useUser(); + const { currentUser, updateTourCompleted } = useUser(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); const { joinedProjectIds } = useProject(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", isLightMode); - const handleTourCompleted = () => { updateTourCompleted() .then(() => { @@ -56,8 +45,6 @@ export const WorkspaceDashboardView = observer(() => { fetchHomeDashboardWidgets(workspaceSlug); }, [fetchHomeDashboardWidgets, workspaceSlug]); - const isEditingAllowed = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; - return ( <> {currentUser && !currentUser.is_tour_completed && ( @@ -78,22 +65,11 @@ export const WorkspaceDashboardView = observer(() => { ) : ( { - setTrackElement("Dashboard empty state"); - toggleCreateProjectModal(true); - }, + type={EmptyStateType.WORKSPACE_DASHBOARD} + primaryButtonOnClick={() => { + setTrackElement("Dashboard empty state"); + toggleCreateProjectModal(true); }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/pages/create-update-page-modal.tsx b/web/components/pages/create-update-page-modal.tsx index eea7e9d7f..c3e22e52e 100644 --- a/web/components/pages/create-update-page-modal.tsx +++ b/web/components/pages/create-update-page-modal.tsx @@ -2,15 +2,15 @@ import React, { FC } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; // components -import { PageForm } from "./page-form"; -// hooks +import { PAGE_CREATED, PAGE_UPDATED } from "constants/event-tracker"; import { useEventTracker } from "hooks/store"; +// hooks // types -import { IPage } from "@plane/types"; import { useProjectPages } from "hooks/store/use-project-page"; import { IPageStore } from "store/page.store"; +import { IPage } from "@plane/types"; +import { PageForm } from "./page-form"; // constants -import { PAGE_CREATED, PAGE_UPDATED } from "constants/event-tracker"; type Props = { // data?: IPage | null; diff --git a/web/components/pages/delete-page-modal.tsx b/web/components/pages/delete-page-modal.tsx index bba19b31c..362dae172 100644 --- a/web/components/pages/delete-page-modal.tsx +++ b/web/components/pages/delete-page-modal.tsx @@ -1,17 +1,16 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useEventTracker, usePage } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; -// types -import { useProjectPages } from "hooks/store/use-project-page"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PAGE_DELETED } from "constants/event-tracker"; +// hooks +import { useEventTracker, usePage } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-page"; +// types type TConfirmPageDeletionProps = { pageId: string; @@ -32,9 +31,6 @@ export const DeletePageModal: React.FC = observer((pr const { capturePageEvent } = useEventTracker(); const pageStore = usePage(pageId); - // toast alert - const { setToastAlert } = useToast(); - if (!pageStore) return null; const { name } = pageStore; @@ -60,8 +56,8 @@ export const DeletePageModal: React.FC = observer((pr }, }); handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Page deleted successfully.", }); @@ -74,8 +70,8 @@ export const DeletePageModal: React.FC = observer((pr state: "FAILED", }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Page could not be deleted. Please try again.", }); diff --git a/web/components/pages/page-form.tsx b/web/components/pages/page-form.tsx index 4f5874e5f..97e881096 100644 --- a/web/components/pages/page-form.tsx +++ b/web/components/pages/page-form.tsx @@ -2,10 +2,10 @@ import { Controller, useForm } from "react-hook-form"; // ui import { Button, Input, Tooltip } from "@plane/ui"; // types -import { IPage } from "@plane/types"; // constants import { PAGE_ACCESS_SPECIFIERS } from "constants/page"; import { IPageStore } from "store/page.store"; +import { IPage } from "@plane/types"; type Props = { handleFormSubmit: (values: IPage) => Promise; diff --git a/web/components/pages/pages-list/all-pages-list.tsx b/web/components/pages/pages-list/all-pages-list.tsx index 4ed759a0f..e7cb21775 100644 --- a/web/components/pages/pages-list/all-pages-list.tsx +++ b/web/components/pages/pages-list/all-pages-list.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const AllPagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/archived-pages-list.tsx b/web/components/pages/pages-list/archived-pages-list.tsx index eb57d7558..f7bcb6059 100644 --- a/web/components/pages/pages-list/archived-pages-list.tsx +++ b/web/components/pages/pages-list/archived-pages-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components +import { Loader, Spinner } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // hooks // ui -import { Loader, Spinner } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const ArchivedPagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/favorite-pages-list.tsx b/web/components/pages/pages-list/favorite-pages-list.tsx index 4ce301a68..4d2ad5019 100644 --- a/web/components/pages/pages-list/favorite-pages-list.tsx +++ b/web/components/pages/pages-list/favorite-pages-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // hooks // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const FavoritePagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/list-item.tsx b/web/components/pages/pages-list/list-item.tsx index 6b1a4793d..d4cb3c023 100644 --- a/web/components/pages/pages-list/list-item.tsx +++ b/web/components/pages/pages-list/list-item.tsx @@ -1,6 +1,7 @@ import { FC, useState } from "react"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; import { AlertCircle, Archive, @@ -13,17 +14,16 @@ import { Star, Trash2, } from "lucide-react"; -import { copyUrlToClipboard } from "helpers/string.helper"; -import { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; // ui import { CustomMenu, Tooltip } from "@plane/ui"; // components import { CreateUpdatePageModal, DeletePageModal } from "components/pages"; // constants import { EUserProjectRoles } from "constants/project"; -import { useRouter } from "next/router"; -import { useProjectPages } from "hooks/store/use-project-specific-pages"; +import { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { useMember, usePage, useUser } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-specific-pages"; import { IIssueLabel } from "@plane/types"; export interface IPagesListItem { diff --git a/web/components/pages/pages-list/list-view.tsx b/web/components/pages/pages-list/list-view.tsx index ebd1fa128..8c1a09e73 100644 --- a/web/components/pages/pages-list/list-view.tsx +++ b/web/components/pages/pages-list/list-view.tsx @@ -1,17 +1,15 @@ import { FC } from "react"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { useApplication, useUser } from "hooks/store"; +import { useApplication } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { PagesListItem } from "./list-item"; // ui import { Loader } from "@plane/ui"; // constants -import { EUserProjectRoles } from "constants/project"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state"; type IPagesListView = { pageIds: string[]; @@ -19,34 +17,20 @@ type IPagesListView = { export const PagesListView: FC = (props) => { const { pageIds: projectPageIds } = props; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreatePageModal }, } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); // local storage const { storedValue: pageTab } = useLocalStorage("pageTab", "Recent"); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const currentPageTabDetails = pageTab - ? PAGE_EMPTY_STATE_DETAILS[pageTab as keyof typeof PAGE_EMPTY_STATE_DETAILS] - : PAGE_EMPTY_STATE_DETAILS["All"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("pages", currentPageTabDetails.key, isLightMode); - - const isButtonVisible = currentPageTabDetails.key !== "archived" && currentPageTabDetails.key !== "favorites"; - // here we are only observing the projectPageStore, so that we can re-render the component when the projectPageStore changes - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const emptyStateType = pageTab ? `project-page-${pageTab}` : EmptyStateType.PROJECT_PAGE_ALL; + const isButtonVisible = pageTab !== "archived" && pageTab !== "favorites"; return ( <> @@ -60,18 +44,8 @@ export const PagesListView: FC = (props) => { ) : ( toggleCreatePageModal(true), - } - : undefined - } - disabled={!isEditingAllowed} + type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} + primaryButtonOnClick={isButtonVisible ? () => toggleCreatePageModal(true) : undefined} /> )}
diff --git a/web/components/pages/pages-list/private-page-list.tsx b/web/components/pages/pages-list/private-page-list.tsx index 15a577d80..316c6bf44 100644 --- a/web/components/pages/pages-list/private-page-list.tsx +++ b/web/components/pages/pages-list/private-page-list.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks // components +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const PrivatePagesList: FC = observer(() => { diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 71bbf12ac..45de8db0d 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -1,39 +1,27 @@ import React, { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks -import { useApplication, useUser } from "hooks/store"; +import { useApplication } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; // components import { PagesListView } from "components/pages/pages-list"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui import { Loader } from "@plane/ui"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // constants -import { EUserProjectRoles } from "constants/project"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; export const RecentPagesList: FC = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); - const { recentProjectPages } = useProjectPages(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", isLightMode); + const { recentProjectPages } = useProjectPages(); // FIXME: replace any with proper type const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - if (!recentProjectPages) { return ( @@ -64,15 +52,9 @@ export const RecentPagesList: FC = observer(() => { ) : ( <> commandPaletteStore.toggleCreatePageModal(true), - }} + type={EmptyStateType.PROJECT_PAGE_RECENT} + primaryButtonOnClick={() => commandPaletteStore.toggleCreatePageModal(true)} size="sm" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/pages/pages-list/shared-pages-list.tsx b/web/components/pages/pages-list/shared-pages-list.tsx index d20a1350e..2626db13c 100644 --- a/web/components/pages/pages-list/shared-pages-list.tsx +++ b/web/components/pages/pages-list/shared-pages-list.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // components +import { Loader } from "@plane/ui"; import { PagesListView } from "components/pages/pages-list"; // hooks // ui -import { Loader } from "@plane/ui"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const SharedPagesList: FC = observer(() => { diff --git a/web/components/profile/activity/activity-list.tsx b/web/components/profile/activity/activity-list.tsx new file mode 100644 index 000000000..e77128e9f --- /dev/null +++ b/web/components/profile/activity/activity-list.tsx @@ -0,0 +1,162 @@ +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { History, MessageSquare } from "lucide-react"; +// editor +// hooks +// components +import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; +// ui +import { ActivitySettingsLoader } from "components/ui"; +// helpers +import { calculateTimeAgo } from "helpers/date-time.helper"; +import { useUser } from "hooks/store"; +// types +import { IUserActivityResponse } from "@plane/types"; + +type Props = { + activity: IUserActivityResponse | undefined; +}; + +export const ActivityList: React.FC = observer((props) => { + const { activity } = props; + // store hooks + const { currentUser } = useUser(); + + // TODO: refactor this component + return ( + <> + {activity ? ( +
    + {activity.results.map((activityItem: any) => { + if (activityItem.field === "comment") + return ( +
    +
    +
    + {activityItem.field ? ( + activityItem.new_value === "restore" && + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
    + {activityItem.actor_detail.display_name?.[0]} +
    + )} + + + +
    +
    +
    +
    + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name + " Bot" + : activityItem.actor_detail.display_name} +
    +

    + Commented {calculateTimeAgo(activityItem.created_at)} +

    +
    +
    + +
    +
    +
    +
    + ); + + const message = + activityItem.verb === "created" && + !["cycles", "modules", "attachment", "link", "estimate"].includes(activityItem.field) && + !activityItem.field ? ( + + created + + ) : ( + + ); + + if ("field" in activityItem && activityItem.field !== "updated_by") + return ( +
  • +
    +
    + <> +
    +
    +
    +
    + {activityItem.field ? ( + activityItem.new_value === "restore" ? ( + + ) : ( + + ) + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
    + {activityItem.actor_detail.display_name?.[0]} +
    + )} +
    +
    +
    +
    +
    +
    + {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( + Plane + ) : activityItem.actor_detail.is_bot ? ( + {activityItem.actor_detail.first_name} Bot + ) : ( + + + {currentUser?.id === activityItem.actor_detail.id + ? "You" + : activityItem.actor_detail.display_name} + + + )}{" "} +
    + {message}{" "} + + {calculateTimeAgo(activityItem.created_at)} + +
    +
    +
    + +
    +
    +
  • + ); + })} +
+ ) : ( + + )} + + ); +}); diff --git a/web/components/profile/activity/download-button.tsx b/web/components/profile/activity/download-button.tsx new file mode 100644 index 000000000..491ebf45f --- /dev/null +++ b/web/components/profile/activity/download-button.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +// services +// ui +import { Button } from "@plane/ui"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { UserService } from "services/user.service"; + +const userService = new UserService(); + +export const DownloadActivityButton = () => { + // states + const [isDownloading, setIsDownloading] = useState(false); + // router + const router = useRouter(); + const { workspaceSlug, userId } = router.query; + + const handleDownload = async () => { + const today = renderFormattedPayloadDate(new Date()); + + if (!workspaceSlug || !userId || !today) return; + + setIsDownloading(true); + + const csv = await userService + .downloadProfileActivity(workspaceSlug.toString(), userId.toString(), { + date: today, + }) + .finally(() => setIsDownloading(false)); + + // create a Blob object + const blob = new Blob([csv], { type: "text/csv" }); + + // create URL for the Blob object + const url = window.URL.createObjectURL(blob); + + // create a link element + const a = document.createElement("a"); + a.href = url; + a.download = `profile-activity-${Date.now()}.csv`; + document.body.appendChild(a); + + // simulate click on the link element to trigger download + a.click(); + + // cleanup + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }; + + return ( + + ); +}; diff --git a/web/components/profile/activity/index.ts b/web/components/profile/activity/index.ts new file mode 100644 index 000000000..3b202d6c5 --- /dev/null +++ b/web/components/profile/activity/index.ts @@ -0,0 +1,4 @@ +export * from "./activity-list"; +export * from "./download-button"; +export * from "./profile-activity-list"; +export * from "./workspace-activity-list"; diff --git a/web/components/profile/activity/profile-activity-list.tsx b/web/components/profile/activity/profile-activity-list.tsx new file mode 100644 index 000000000..6311c382c --- /dev/null +++ b/web/components/profile/activity/profile-activity-list.tsx @@ -0,0 +1,190 @@ +import { useEffect } from "react"; +import { RichReadOnlyEditor } from "@plane/rich-text-editor"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import useSWR from "swr"; +import { History, MessageSquare } from "lucide-react"; +// hooks +import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; +import { ActivitySettingsLoader } from "components/ui"; +import { USER_ACTIVITY } from "constants/fetch-keys"; +import { calculateTimeAgo } from "helpers/date-time.helper"; +import { useUser } from "hooks/store"; +// services +import { UserService } from "services/user.service"; +// editor +// components +// ui +// helpers +// fetch-keys + +// services +const userService = new UserService(); + +type Props = { + cursor: string; + perPage: number; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; +}; + +export const ProfileActivityListPage: React.FC = observer((props) => { + const { cursor, perPage, updateResultsCount, updateTotalPages } = props; + // store hooks + const { currentUser } = useUser(); + + const { data: userProfileActivity } = useSWR( + USER_ACTIVITY({ + cursor, + }), + () => + userService.getUserActivity({ + cursor, + per_page: perPage, + }) + ); + + useEffect(() => { + if (!userProfileActivity) return; + + updateTotalPages(userProfileActivity.total_pages); + updateResultsCount(userProfileActivity.results.length); + }, [updateResultsCount, updateTotalPages, userProfileActivity]); + + // TODO: refactor this component + return ( + <> + {userProfileActivity ? ( +
    + {userProfileActivity.results.map((activityItem: any) => { + if (activityItem.field === "comment") + return ( +
    +
    +
    + {activityItem.field ? ( + activityItem.new_value === "restore" && + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
    + {activityItem.actor_detail.display_name?.[0]} +
    + )} + + + +
    +
    +
    +
    + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name + " Bot" + : activityItem.actor_detail.display_name} +
    +

    + Commented {calculateTimeAgo(activityItem.created_at)} +

    +
    +
    + +
    +
    +
    +
    + ); + + const message = + activityItem.verb === "created" && + !["cycles", "modules", "attachment", "link", "estimate"].includes(activityItem.field) && + !activityItem.field ? ( + + created + + ) : ( + + ); + + if ("field" in activityItem && activityItem.field !== "updated_by") + return ( +
  • +
    +
    + <> +
    +
    +
    +
    + {activityItem.field ? ( + activityItem.new_value === "restore" ? ( + + ) : ( + + ) + ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
    + {activityItem.actor_detail.display_name?.[0]} +
    + )} +
    +
    +
    +
    +
    +
    + {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( + Plane + ) : activityItem.actor_detail.is_bot ? ( + {activityItem.actor_detail.first_name} Bot + ) : ( + + + {currentUser?.id === activityItem.actor_detail.id + ? "You" + : activityItem.actor_detail.display_name} + + + )}{" "} +
    + {message}{" "} + + {calculateTimeAgo(activityItem.created_at)} + +
    +
    +
    + +
    +
    +
  • + ); + })} +
+ ) : ( + + )} + + ); +}); diff --git a/web/components/profile/activity/workspace-activity-list.tsx b/web/components/profile/activity/workspace-activity-list.tsx new file mode 100644 index 000000000..aa5a03dee --- /dev/null +++ b/web/components/profile/activity/workspace-activity-list.tsx @@ -0,0 +1,50 @@ +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// services +import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; +import { UserService } from "services/user.service"; +// components +import { ActivityList } from "./activity-list"; +// fetch-keys + +// services +const userService = new UserService(); + +type Props = { + cursor: string; + perPage: number; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; +}; + +export const WorkspaceActivityListPage: React.FC = (props) => { + const { cursor, perPage, updateResultsCount, updateTotalPages } = props; + // router + const router = useRouter(); + const { workspaceSlug, userId } = router.query; + + const { data: userProfileActivity } = useSWR( + workspaceSlug && userId + ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString(), { + cursor, + }) + : null, + workspaceSlug && userId + ? () => + userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString(), { + cursor, + per_page: perPage, + }) + : null + ); + + useEffect(() => { + if (!userProfileActivity) return; + + updateTotalPages(userProfileActivity.total_pages); + updateResultsCount(userProfileActivity.results.length); + }, [updateResultsCount, updateTotalPages, userProfileActivity]); + + return ; +}; diff --git a/web/components/profile/index.ts b/web/components/profile/index.ts index f6d2a3775..35ac288ad 100644 --- a/web/components/profile/index.ts +++ b/web/components/profile/index.ts @@ -1,3 +1,4 @@ +export * from "./activity"; export * from "./overview"; export * from "./navbar"; export * from "./profile-issues-filter"; diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index 4361b7a9d..ecc0028db 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // components import { ProfileIssuesFilter } from "components/profile"; @@ -27,10 +27,11 @@ export const ProfileNavbar: React.FC = (props) => { {tabsList.map((tab) => ( {tab.label} diff --git a/web/components/profile/overview/activity.tsx b/web/components/profile/overview/activity.tsx index 58bbb6898..c8af1ccf8 100644 --- a/web/components/profile/overview/activity.tsx +++ b/web/components/profile/overview/activity.tsx @@ -1,21 +1,21 @@ +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; //hooks +import { Loader } from "@plane/ui"; +import { ActivityMessage, IssueLink } from "components/core"; +import { ProfileEmptyState } from "components/ui"; +import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; +import { calculateTimeAgo } from "helpers/date-time.helper"; import { useUser } from "hooks/store"; // services +import recentActivityEmptyState from "public/empty-state/recent_activity.svg"; import { UserService } from "services/user.service"; // components -import { ActivityMessage, IssueLink } from "components/core"; // ui -import { ProfileEmptyState } from "components/ui"; -import { Loader } from "@plane/ui"; // image -import recentActivityEmptyState from "public/empty-state/recent_activity.svg"; // helpers -import { calculateTimeAgo } from "helpers/date-time.helper"; // fetch-keys -import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; // services const userService = new UserService(); @@ -27,15 +27,18 @@ export const ProfileActivity = observer(() => { const { currentUser } = useUser(); const { data: userProfileActivity } = useSWR( - workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString()) : null, + workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString(), {}) : null, workspaceSlug && userId - ? () => userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString()) + ? () => + userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString(), { + per_page: 10, + }) : null ); return (
-

Recent Activity

+

Recent activity

{userProfileActivity ? ( userProfileActivity.results.length > 0 ? ( @@ -43,24 +46,24 @@ export const ProfileActivity = observer(() => { {userProfileActivity.results.map((activity) => (
- {activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( + {activity.actor_detail?.avatar && activity.actor_detail?.avatar !== "" ? ( {activity.actor_detail.display_name} ) : (
- {activity.actor_detail.display_name?.charAt(0)} + {activity.actor_detail?.display_name?.charAt(0)}
)}

- {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail.display_name}{" "} + {currentUser?.id === activity.actor_detail?.id ? "You" : activity.actor_detail?.display_name}{" "} {activity.field ? ( diff --git a/web/components/profile/overview/priority-distribution.tsx b/web/components/profile/overview/priority-distribution.tsx index 8a931183f..12e430409 100644 --- a/web/components/profile/overview/priority-distribution.tsx +++ b/web/components/profile/overview/priority-distribution.tsx @@ -1,10 +1,10 @@ // ui -import { BarGraph, ProfileEmptyState } from "components/ui"; import { Loader } from "@plane/ui"; +import { BarGraph, ProfileEmptyState } from "components/ui"; // image +import { capitalizeFirstLetter } from "helpers/string.helper"; import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; // helpers -import { capitalizeFirstLetter } from "helpers/string.helper"; // types import { IUserProfileData } from "@plane/types"; diff --git a/web/components/profile/overview/priority-distribution/index.ts b/web/components/profile/overview/priority-distribution/index.ts new file mode 100644 index 000000000..64d81eb12 --- /dev/null +++ b/web/components/profile/overview/priority-distribution/index.ts @@ -0,0 +1 @@ +export * from "./priority-distribution"; diff --git a/web/components/profile/overview/priority-distribution/main-content.tsx b/web/components/profile/overview/priority-distribution/main-content.tsx new file mode 100644 index 000000000..8606f44b1 --- /dev/null +++ b/web/components/profile/overview/priority-distribution/main-content.tsx @@ -0,0 +1,31 @@ +// components +import { IssuesByPriorityGraph } from "components/graphs"; +import { ProfileEmptyState } from "components/ui"; +// assets +import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; +// types +import { IUserPriorityDistribution } from "@plane/types"; + +type Props = { + priorityDistribution: IUserPriorityDistribution[]; +}; + +export const PriorityDistributionContent: React.FC = (props) => { + const { priorityDistribution } = props; + + return ( +

+ {priorityDistribution.length > 0 ? ( + + ) : ( +
+ +
+ )} +
+ ); +}; diff --git a/web/components/profile/overview/priority-distribution/priority-distribution.tsx b/web/components/profile/overview/priority-distribution/priority-distribution.tsx new file mode 100644 index 000000000..48d612dff --- /dev/null +++ b/web/components/profile/overview/priority-distribution/priority-distribution.tsx @@ -0,0 +1,33 @@ +// components +// ui +import { Loader } from "@plane/ui"; +// types +import { IUserPriorityDistribution } from "@plane/types"; +import { PriorityDistributionContent } from "./main-content"; + +type Props = { + priorityDistribution: IUserPriorityDistribution[] | undefined; +}; + +export const ProfilePriorityDistribution: React.FC = (props) => { + const { priorityDistribution } = props; + + return ( +
+

Issues by priority

+ {priorityDistribution ? ( + + ) : ( +
+ + + + + + + +
+ )} +
+ ); +}; diff --git a/web/components/profile/overview/state-distribution.tsx b/web/components/profile/overview/state-distribution.tsx index 5664637e9..25de06c84 100644 --- a/web/components/profile/overview/state-distribution.tsx +++ b/web/components/profile/overview/state-distribution.tsx @@ -1,11 +1,11 @@ // ui import { ProfileEmptyState, PieGraph } from "components/ui"; // image +import { STATE_GROUPS } from "constants/state"; import stateGraph from "public/empty-state/state_graph.svg"; // types import { IUserProfileData, IUserStateDistribution } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; type Props = { stateDistribution: IUserStateDistribution[]; @@ -17,7 +17,7 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u return (
-

Issues by State

+

Issues by state

{userProfile.state_distribution.length > 0 ? (
@@ -65,7 +65,7 @@ export const ProfileStateDistribution: React.FC = ({ stateDistribution, u backgroundColor: STATE_GROUPS[group.state_group].color, }} /> -
{group.state_group}
+
{STATE_GROUPS[group.state_group].label}
{group.state_count}
diff --git a/web/components/profile/overview/stats.tsx b/web/components/profile/overview/stats.tsx index 3f96488a0..62873ee38 100644 --- a/web/components/profile/overview/stats.tsx +++ b/web/components/profile/overview/stats.tsx @@ -1,9 +1,9 @@ -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // ui -import { CreateIcon, LayerStackIcon, Loader } from "@plane/ui"; import { UserCircle2 } from "lucide-react"; +import { CreateIcon, LayerStackIcon, Loader } from "@plane/ui"; // types import { IUserProfileData } from "@plane/types"; diff --git a/web/components/profile/overview/workload.tsx b/web/components/profile/overview/workload.tsx index c091a94f7..c7e7a94a0 100644 --- a/web/components/profile/overview/workload.tsx +++ b/web/components/profile/overview/workload.tsx @@ -1,7 +1,7 @@ // types +import { STATE_GROUPS } from "constants/state"; import { IUserStateDistribution } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; type Props = { stateDistribution: IUserStateDistribution[]; @@ -21,12 +21,12 @@ export const ProfileWorkload: React.FC = ({ stateDistribution }) => ( }} />
-

+

{group.state_group === "unstarted" - ? "Not Started" + ? "Not started" : group.state_group === "started" - ? "Working on" - : group.state_group} + ? "Working on" + : STATE_GROUPS[group.state_group].label}

{group.state_count}

diff --git a/web/components/profile/preferences/email-notification-form.tsx b/web/components/profile/preferences/email-notification-form.tsx index e041b28d8..fd158e2d5 100644 --- a/web/components/profile/preferences/email-notification-form.tsx +++ b/web/components/profile/preferences/email-notification-form.tsx @@ -1,9 +1,7 @@ import React, { FC } from "react"; import { Controller, useForm } from "react-hook-form"; // ui -import { Button, Checkbox } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; +import { Button, Checkbox, TOAST_TYPE, setToast } from "@plane/ui"; // services import { UserService } from "services/user.service"; // types @@ -18,8 +16,6 @@ const userService = new UserService(); export const EmailNotificationForm: FC = (props) => { const { data } = props; - // toast - const { setToastAlert } = useToast(); // form data const { handleSubmit, @@ -45,9 +41,9 @@ export const EmailNotificationForm: FC = (props) => await userService .updateCurrentUserEmailNotificationSettings(payload) .then(() => - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Email Notification Settings updated successfully", }) ) diff --git a/web/components/profile/preferences/index.ts b/web/components/profile/preferences/index.ts index ddda5712c..56ef42216 100644 --- a/web/components/profile/preferences/index.ts +++ b/web/components/profile/preferences/index.ts @@ -1 +1 @@ -export * from "./email-notification-form"; \ No newline at end of file +export * from "./email-notification-form"; diff --git a/web/components/profile/profile-issues-filter.tsx b/web/components/profile/profile-issues-filter.tsx index 5b4f9c8ed..491c00f3a 100644 --- a/web/components/profile/profile-issues-filter.tsx +++ b/web/components/profile/profile-issues-filter.tsx @@ -4,9 +4,9 @@ import { useRouter } from "next/router"; // components import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "components/issues"; // hooks +import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { useIssues, useLabel } from "hooks/store"; // constants -import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; export const ProfileIssuesFilter = observer(() => { diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index 7e501764a..f94c1d91f 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -1,20 +1,18 @@ import React, { useEffect } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // components -import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; -import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues"; +import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; +import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // hooks -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { EIssuesStoreType } from "constants/issue"; -import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; interface IProfileIssuesPage { type: "assigned" | "subscribed" | "created"; @@ -28,21 +26,15 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { workspaceSlug: string; userId: string; }; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); const { issues: { loader, groupedIssueIds, fetchIssues, setViewId }, issuesFilter: { issueFilters, fetchFilters }, } = useIssues(EIssuesStoreType.PROFILE); useEffect(() => { - setViewId(type); - }, [type]); + if (setViewId) setViewId(type); + }, [type, setViewId]); useSWR( workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${type}` : null, @@ -55,26 +47,15 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { { revalidateIfStale: false, revalidateOnFocus: false } ); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("profile", type, isLightMode); - const activeLayout = issueFilters?.displayFilters?.layout || undefined; - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const emptyStateType = `profile-${type}`; if (!groupedIssueIds || loader === "init-loader") return <>{activeLayout === "list" ? : }; if (groupedIssueIds.length === 0) { - return ( - - ); + return ; } return ( diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 107c1f528..4cab1a9f1 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -1,37 +1,40 @@ -import { useRouter } from "next/router"; -import Link from "next/link"; -import useSWR from "swr"; -import { Disclosure, Transition } from "@headlessui/react"; +import { useEffect, useRef } from "react"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// ui +import { Disclosure, Transition } from "@headlessui/react"; +// icons +import { ChevronDown, Pencil } from "lucide-react"; +// plane ui +import { Loader, Tooltip } from "@plane/ui"; +// fetch-keys +import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; // hooks -import { useApplication, useUser } from "hooks/store"; +import { useApplication, useProject, useUser } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // services import { UserService } from "services/user.service"; // components import { ProfileSidebarTime } from "./time"; -// ui -import { Loader, Tooltip } from "@plane/ui"; -// icons -import { ChevronDown, Pencil } from "lucide-react"; -// helpers -import { renderFormattedDate } from "helpers/date-time.helper"; -import { renderEmoji } from "helpers/emoji.helper"; -// fetch-keys -import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -import { useEffect, useRef } from "react"; +import { ProjectLogo } from "components/project"; // services const userService = new UserService(); export const ProfileSidebar = observer(() => { + // refs + const ref = useRef(null); // router const router = useRouter(); const { workspaceSlug, userId } = router.query; // store hooks const { currentUser } = useUser(); const { theme: themeStore } = useApplication(); - const ref = useRef(null); + const { getProjectById } = useProject(); const { data: userProjectsData } = useSWR( workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null, @@ -76,7 +79,7 @@ export const ProfileSidebar = observer(() => { return (
{userProjectsData ? ( @@ -93,21 +96,22 @@ export const ProfileSidebar = observer(() => { )} {userProjectsData.user_data.display_name}
- {userProjectsData.user_data.avatar && userProjectsData.user_data.avatar !== "" ? ( + {userProjectsData.user_data?.avatar && userProjectsData.user_data?.avatar !== "" ? ( {userProjectsData.user_data.display_name} ) : ( -
- {userProjectsData.user_data.first_name?.[0]} +
+ {userProjectsData.user_data?.first_name?.[0]}
)}
@@ -115,9 +119,9 @@ export const ProfileSidebar = observer(() => {

- {userProjectsData.user_data.first_name} {userProjectsData.user_data.last_name} + {userProjectsData.user_data?.first_name} {userProjectsData.user_data?.last_name}

-
({userProjectsData.user_data.display_name})
+
({userProjectsData.user_data?.display_name})
{userDetails.map((detail) => ( @@ -129,6 +133,8 @@ export const ProfileSidebar = observer(() => {
{userProjectsData.project_data.map((project, index) => { + const projectDetails = getProjectById(project.id); + const totalIssues = project.created_issues + project.assigned_issues + project.pending_issues + project.completed_issues; @@ -137,37 +143,30 @@ export const ProfileSidebar = observer(() => { ? 0 : Math.round((project.completed_issues / project.assigned_issues) * 100); + if (!projectDetails) return null; + return ( {({ open }) => (
- {project.emoji ? ( -
- {renderEmoji(project.emoji)} -
- ) : project.icon_prop ? ( -
- {renderEmoji(project.icon_prop)} -
- ) : ( -
- {project?.name.charAt(0)} -
- )} -
{project.name}
+ + + +
{projectDetails.name}
{project.assigned_issues > 0 && (
{completedIssuePercentage}%
diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index 624e30bde..df63dfb73 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -1,32 +1,20 @@ import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useProject } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; import { ProjectCard } from "components/project"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { ProjectsLoader } from "components/ui"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; export const ProjectCardList = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); + const { workspaceProjectIds, searchedProjects, getProjectById } = useProject(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", isLightMode); - - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - if (!workspaceProjectIds) return ; return ( @@ -49,22 +37,11 @@ export const ProjectCardList = observer(() => {
) : ( { - setTrackElement("Project empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - }, + type={EmptyStateType.WORKSPACE_PROJECTS} + primaryButtonOnClick={() => { + setTrackElement("Project empty state"); + commandPaletteStore.toggleCreateProjectModal(true); }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 3d6f62b7b..08aec43fa 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -1,22 +1,20 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; import Link from "next/link"; -// hooks -import { useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components -import { DeleteProjectModal, JoinProjectModal } from "components/project"; +import { useRouter } from "next/router"; +import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; // ui -import { Avatar, AvatarGroup, Button, Tooltip } from "@plane/ui"; +import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; +// components +import { DeleteProjectModal, JoinProjectModal, ProjectLogo } from "components/project"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; -import { renderEmoji } from "helpers/emoji.helper"; +// hooks +import { useProject } from "hooks/store"; // types import type { IProject } from "@plane/types"; -// constants import { EUserProjectRoles } from "constants/project"; +// constants export type ProjectCardProps = { project: IProject; @@ -27,8 +25,6 @@ export const ProjectCard: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast alert - const { setToastAlert } = useToast(); // states const [deleteProjectModalOpen, setDeleteProjectModal] = useState(false); const [joinProjectModalOpen, setJoinProjectModal] = useState(false); @@ -42,24 +38,34 @@ export const ProjectCard: React.FC = observer((props) => { const handleAddToFavorites = () => { if (!workspaceSlug) return; - addProjectToFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const addToFavoritePromise = addProjectToFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(addToFavoritePromise, { + loading: "Adding project to favorites...", + success: { + title: "Success!", + message: () => "Project added to favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't add the project to favorites. Please try again.", + }, }); }; const handleRemoveFromFavorites = () => { if (!workspaceSlug || !project) return; - removeProjectFromFavorites(workspaceSlug.toString(), project.id).catch(() => { - setToastAlert({ - type: "error", + const removeFromFavoritePromise = removeProjectFromFavorites(workspaceSlug.toString(), project.id); + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing project from favorites...", + success: { + title: "Success!", + message: () => "Project removed from favorites.", + }, + error: { title: "Error!", - message: "Couldn't remove the project from favorites. Please try again.", - }); + message: () => "Couldn't remove the project from favorites. Please try again.", + }, }); }; @@ -67,8 +73,8 @@ export const ProjectCard: React.FC = observer((props) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${project.id}/issues`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Project link copied to clipboard.", }); @@ -117,13 +123,9 @@ export const ProjectCard: React.FC = observer((props) => {
-
- - {project.emoji - ? renderEmoji(project.emoji) - : project.icon_prop - ? renderEmoji(project.icon_prop) - : null} +
+ +
diff --git a/web/components/project/confirm-project-member-remove.tsx b/web/components/project/confirm-project-member-remove.tsx index 7ab4afa0a..2c94c092d 100644 --- a/web/components/project/confirm-project-member-remove.tsx +++ b/web/components/project/confirm-project-member-remove.tsx @@ -1,11 +1,11 @@ import React, { useState } from "react"; -import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks +import { Button } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // types import { IUserLite } from "@plane/types"; @@ -94,8 +94,8 @@ export const ConfirmProjectMemberRemove: React.FC = observer((props) => { ? "Leaving..." : "Leave" : isDeleteLoading - ? "Removing..." - : "Remove"} + ? "Removing..." + : "Remove"}
diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 49a42a0a3..4d30c3dec 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -1,24 +1,33 @@ import { useState, useEffect, Fragment, FC, ChangeEvent } from "react"; +import { observer } from "mobx-react-lite"; import { useForm, Controller } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; -// hooks -import { useEventTracker, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; +import { + Button, + CustomEmojiIconPicker, + CustomSelect, + EmojiIconPickerTypes, + Input, + setToast, + TextArea, + TOAST_TYPE, +} from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; -import EmojiIconPicker from "components/emoji-icon-picker"; import { MemberDropdown } from "components/dropdowns"; -// helpers -import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; // constants -import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { PROJECT_CREATED } from "constants/event-tracker"; +import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; +// helpers +import { convertHexEmojiToDecimal, getRandomEmoji } from "helpers/emoji.helper"; +// hooks +import { useEventTracker, useProject, useUser } from "hooks/store"; +import { projectIdentifierSanitizer } from "helpers/project.helper"; +import { ProjectLogo } from "./project-logo"; +import { IProject } from "@plane/types"; type Props = { isOpen: boolean; @@ -31,34 +40,34 @@ interface IIsGuestCondition { onClose: () => void; } -const IsGuestCondition: FC = ({ onClose }) => { - const { setToastAlert } = useToast(); +const defaultValues: Partial = { + cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], + description: "", + logo_props: { + in_use: "emoji", + emoji: { + value: getRandomEmoji(), + }, + }, + identifier: "", + name: "", + network: 2, + project_lead: null, +}; +const IsGuestCondition: FC = ({ onClose }) => { useEffect(() => { onClose(); - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: "You don't have permission to create project.", }); - }, [onClose, setToastAlert]); + }, [onClose]); return null; }; -export interface ICreateProjectForm { - name: string; - identifier: string; - description: string; - emoji_and_icon: string; - network: number; - project_lead_member: string; - project_lead: string; - cover_image: string; - icon_prop: any; - emoji: string; -} - export const CreateProjectModal: FC = observer((props) => { const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; // store @@ -69,10 +78,7 @@ export const CreateProjectModal: FC = observer((props) => { const { addProjectToFavorites, createProject } = useProject(); // states const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); - // toast - const { setToastAlert } = useToast(); // form info - const cover_image = PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)]; const { formState: { errors, isSubmitting }, handleSubmit, @@ -80,54 +86,39 @@ export const CreateProjectModal: FC = observer((props) => { control, watch, setValue, - } = useForm({ - defaultValues: { - cover_image, - description: "", - emoji_and_icon: getRandomEmoji(), - identifier: "", - name: "", - network: 2, - project_lead: undefined, - }, + } = useForm({ + defaultValues, reValidateMode: "onChange", }); - const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network")); - if (currentWorkspaceRole && isOpen) if (currentWorkspaceRole < EUserWorkspaceRoles.MEMBER) return ; const handleClose = () => { onClose(); setIsChangeInIdentifierRequired(true); - reset(); + setTimeout(() => { + reset(); + }, 300); }; const handleAddToFavorites = (projectId: string) => { if (!workspaceSlug) return; addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Couldn't remove the project from favorites. Please try again.", }); }); }; - const onSubmit = async (formData: ICreateProjectForm) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { emoji_and_icon, project_lead_member, ...payload } = formData; - - if (typeof formData.emoji_and_icon === "object") payload.icon_prop = formData.emoji_and_icon; - else payload.emoji = formData.emoji_and_icon; - - payload.project_lead = formData.project_lead_member; + const onSubmit = async (formData: Partial) => { // Upper case identifier - payload.identifier = payload.identifier.toUpperCase(); + formData.identifier = formData.identifier?.toUpperCase(); - return createProject(workspaceSlug.toString(), payload) + return createProject(workspaceSlug.toString(), formData) .then((res) => { const newPayload = { ...res, @@ -137,8 +128,8 @@ export const CreateProjectModal: FC = observer((props) => { eventName: PROJECT_CREATED, payload: newPayload, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project created successfully.", }); @@ -149,15 +140,15 @@ export const CreateProjectModal: FC = observer((props) => { }) .catch((err) => { Object.keys(err.data).map((key) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.data[key], }); captureProjectEvent({ eventName: PROJECT_CREATED, payload: { - ...payload, + ...formData, state: "FAILED", }, }); @@ -171,13 +162,13 @@ export const CreateProjectModal: FC = observer((props) => { return; } if (e.target.value === "") setValue("identifier", ""); - else setValue("identifier", e.target.value.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "").substring(0, 5)); + else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5)); onChange(e); }; const handleIdentifierChange = (onChange: any) => (e: ChangeEvent) => { const { value } = e.target; - const alphanumericValue = value.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); + const alphanumericValue = projectIdentifierSanitizer(value); setIsChangeInIdentifierRequired(false); onChange(alphanumericValue); }; @@ -210,11 +201,11 @@ export const CreateProjectModal: FC = observer((props) => { >
- {watch("cover_image") !== null && ( + {watch("cover_image") && ( Cover Image )} @@ -224,30 +215,50 @@ export const CreateProjectModal: FC = observer((props) => {
- { - setValue("cover_image", image); - }} + ( + + )} />
( - - {value ? renderEmoji(value) : "Icon"} -
+ + + + } + onChange={(val: any) => { + let logoValue = {}; + + if (val.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val.type === "icon") logoValue = val.value; + + onChange({ + in_use: val.type, + [val.type]: logoValue, + }); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={ + value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON } - onChange={onChange} - value={value} - tabIndex={10} /> )} /> @@ -281,7 +292,9 @@ export const CreateProjectModal: FC = observer((props) => { /> )} /> - {errors?.name?.message} + + <>{errors?.name?.message} +
= observer((props) => { onChange={handleIdentifierChange(onChange)} hasError={Boolean(errors.identifier)} placeholder="Identifier" - className="w-full text-xs focus:border-blue-400 uppercase" + className="w-full text-xs uppercase focus:border-blue-400" tabIndex={2} /> )} /> - {errors?.identifier?.message} + + <>{errors?.identifier?.message} +
= observer((props) => { ( -
- - {currentNetwork ? ( - <> - - {currentNetwork.label} - - ) : ( - Select Network - )} -
- } - placement="bottom-start" - noChevron - tabIndex={4} - > - {NETWORK_CHOICES.map((network) => ( - -
- -
-

{network.label}

-

{network.description}

-
+ render={({ field: { onChange, value } }) => { + const currentNetwork = NETWORK_CHOICES.find((n) => n.key === value); + + return ( +
+ + {currentNetwork ? ( + <> + + {currentNetwork.label} + + ) : ( + Select network + )}
- - ))} - -
- )} + } + placement="bottom-start" + noChevron + tabIndex={4} + > + {NETWORK_CHOICES.map((network) => ( + +
+ +
+

{network.label}

+

{network.description}

+
+
+
+ ))} + +
+ ); + }} /> ( -
- -
- )} + render={({ field: { value, onChange } }) => { + if (value === undefined || value === null || typeof value === "string") + return ( +
+ +
+ ); + else return <>; + }} />
@@ -402,7 +425,7 @@ export const CreateProjectModal: FC = observer((props) => { Cancel
diff --git a/web/components/project/delete-project-modal.tsx b/web/components/project/delete-project-modal.tsx index 791ac3672..ae3acfe4e 100644 --- a/web/components/project/delete-project-modal.tsx +++ b/web/components/project/delete-project-modal.tsx @@ -4,14 +4,13 @@ import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks -import { useEventTracker, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { useEventTracker, useProject } from "hooks/store"; // ui -import { Button, Input } from "@plane/ui"; // types import type { IProject } from "@plane/types"; -// constants import { PROJECT_DELETED } from "constants/event-tracker"; +// constants type DeleteProjectModal = { isOpen: boolean; @@ -28,13 +27,10 @@ export const DeleteProjectModal: React.FC = (props) => { const { isOpen, project, onClose } = props; // store hooks const { captureProjectEvent } = useEventTracker(); - const { currentWorkspace } = useWorkspace(); const { deleteProject } = useProject(); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -67,8 +63,8 @@ export const DeleteProjectModal: React.FC = (props) => { eventName: PROJECT_DELETED, payload: { ...project, state: "SUCCESS", element: "Project general settings" }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project deleted successfully.", }); @@ -78,8 +74,8 @@ export const DeleteProjectModal: React.FC = (props) => { eventName: PROJECT_DELETED, payload: { ...project, state: "FAILED", element: "Project general settings" }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again later.", }); diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index 267103dc8..1ef7ee226 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -1,24 +1,33 @@ import { FC, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; -// hooks -import { useEventTracker, useProject } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components -import EmojiIconPicker from "components/emoji-icon-picker"; -import { ImagePickerPopover } from "components/core"; -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; // icons import { Lock } from "lucide-react"; -// types -import { IProject, IWorkspace } from "@plane/types"; -// helpers -import { renderEmoji } from "helpers/emoji.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; +// ui +import { + Button, + CustomSelect, + Input, + TextArea, + TOAST_TYPE, + setToast, + CustomEmojiIconPicker, + EmojiIconPickerTypes, +} from "@plane/ui"; +// components +import { ImagePickerPopover } from "components/core"; // constants +import { PROJECT_UPDATED } from "constants/event-tracker"; import { NETWORK_CHOICES } from "constants/project"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +// hooks +import { useEventTracker, useProject } from "hooks/store"; // services import { ProjectService } from "services/project"; -import { PROJECT_UPDATED } from "constants/event-tracker"; +// types +import { IProject, IWorkspace } from "@plane/types"; +import { ProjectLogo } from "./project-logo"; +import { convertHexEmojiToDecimal } from "helpers/emoji.helper"; export interface IProjectDetailsForm { project: IProject; workspaceSlug: string; @@ -33,8 +42,6 @@ export const ProjectDetailsForm: FC = (props) => { // store hooks const { captureProjectEvent } = useEventTracker(); const { updateProject } = useProject(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -48,7 +55,6 @@ export const ProjectDetailsForm: FC = (props) => { } = useForm({ defaultValues: { ...project, - emoji_and_icon: project.emoji ?? project.icon_prop, workspace: (project.workspace as IWorkspace).id, }, }); @@ -57,7 +63,6 @@ export const ProjectDetailsForm: FC = (props) => { if (project && projectId !== getValues("id")) { reset({ ...project, - emoji_and_icon: project.emoji ?? project.icon_prop, workspace: (project.workspace as IWorkspace).id, }); } @@ -84,8 +89,8 @@ export const ProjectDetailsForm: FC = (props) => { element: "Project general settings", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Project updated successfully", }); @@ -95,8 +100,8 @@ export const ProjectDetailsForm: FC = (props) => { eventName: PROJECT_UPDATED, payload: { ...payload, state: "FAILED", element: "Project general settings" }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Project could not be updated. Please try again.", }); @@ -111,14 +116,9 @@ export const ProjectDetailsForm: FC = (props) => { identifier: formData.identifier, description: formData.description, cover_image: formData.cover_image, + logo_props: formData.logo_props, }; - if (typeof formData.emoji_and_icon === "object") { - payload.emoji = null; - payload.icon_prop = formData.emoji_and_icon; - } else { - payload.emoji = formData.emoji_and_icon; - payload.icon_prop = null; - } + if (project.identifier !== formData.identifier) await projectService .checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "") @@ -141,20 +141,37 @@ export const ProjectDetailsForm: FC = (props) => {
-
- ( - - )} - /> -
+ ( + + + + } + onChange={(val) => { + let logoValue = {}; + + if (val.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val.type === "icon") logoValue = val.value; + + onChange({ + in_use: val.type, + [val.type]: logoValue, + }); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON} + disabled={!isAdmin} + /> + )} + />
{watch("name")} diff --git a/web/components/project/index.ts b/web/components/project/index.ts index 42e310edb..27f3eda33 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -14,6 +14,7 @@ export * from "./sidebar-list"; export * from "./integration-card"; export * from "./member-list"; export * from "./member-list-item"; +export * from "./project-logo"; export * from "./project-settings-member-defaults"; export * from "./send-project-invitation-modal"; export * from "./confirm-project-member-remove"; diff --git a/web/components/project/integration-card.tsx b/web/components/project/integration-card.tsx index d2910b34a..17f490bc3 100644 --- a/web/components/project/integration-card.tsx +++ b/web/components/project/integration-card.tsx @@ -1,23 +1,19 @@ import React from "react"; - import Image from "next/image"; - -import useSWR, { mutate } from "swr"; - -// services -import { ProjectService } from "services/project"; -// hooks import { useRouter } from "next/router"; -import useToast from "hooks/use-toast"; +import useSWR, { mutate } from "swr"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { SelectRepository, SelectChannel } from "components/integration"; +// constants +import { PROJECT_GITHUB_REPOSITORY } from "constants/fetch-keys"; // icons import GithubLogo from "public/logos/github-square.png"; import SlackLogo from "public/services/slack.png"; // types import { IWorkspaceIntegration } from "@plane/types"; -// fetch-keys -import { PROJECT_GITHUB_REPOSITORY } from "constants/fetch-keys"; +import { ProjectService } from "services/project"; type Props = { integration: IWorkspaceIntegration; @@ -41,8 +37,6 @@ export const IntegrationCard: React.FC = ({ integration }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { setToastAlert } = useToast(); - const { data: syncedGithubRepository } = useSWR( projectId ? PROJECT_GITHUB_REPOSITORY(projectId as string) : null, () => @@ -71,16 +65,16 @@ export const IntegrationCard: React.FC = ({ integration }) => { .then(() => { mutate(PROJECT_GITHUB_REPOSITORY(projectId as string)); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: `${login}/${name} repository synced with the project successfully.`, }); }) .catch((err) => { console.error(err); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Repository could not be synced with the project. Please try again.", }); diff --git a/web/components/project/join-project-modal.tsx b/web/components/project/join-project-modal.tsx index 58b549b6c..384333581 100644 --- a/web/components/project/join-project-modal.tsx +++ b/web/components/project/join-project-modal.tsx @@ -2,9 +2,9 @@ import { useState, Fragment } from "react"; import { useRouter } from "next/router"; import { Transition, Dialog } from "@headlessui/react"; // hooks +import { Button } from "@plane/ui"; import { useProject, useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; // types import type { IProject } from "@plane/types"; diff --git a/web/components/project/leave-project-modal.tsx b/web/components/project/leave-project-modal.tsx index 0827568ce..6982d6316 100644 --- a/web/components/project/leave-project-modal.tsx +++ b/web/components/project/leave-project-modal.tsx @@ -1,18 +1,19 @@ import { FC, Fragment } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; +// headless ui import { Dialog, Transition } from "@headlessui/react"; +// icons import { AlertTriangleIcon } from "lucide-react"; -import { observer } from "mobx-react-lite"; -// hooks -import { useEventTracker, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; -// types -import { IProject } from "@plane/types"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker"; +// hooks +import { useEventTracker, useUser } from "hooks/store"; +// types +import { IProject } from "@plane/types"; type FormData = { projectName: string; @@ -40,8 +41,6 @@ export const LeaveProjectModal: FC = observer((props) => { const { membership: { leaveProject }, } = useUser(); - // toast - const { setToastAlert } = useToast(); const { control, @@ -71,8 +70,8 @@ export const LeaveProjectModal: FC = observer((props) => { }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong please try again later.", }); @@ -82,22 +81,22 @@ export const LeaveProjectModal: FC = observer((props) => { }); }); } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please confirm leaving the project by typing the 'Leave Project'.", }); } } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please enter the project name as shown in the description.", }); } } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please fill all fields.", }); diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 6a27eccd5..55b1b3c9a 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -1,20 +1,19 @@ import { useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; -// hooks -import { useEventTracker, useMember, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components -import { ConfirmProjectMemberRemove } from "components/project"; -// ui -import { CustomSelect, Tooltip } from "@plane/ui"; +import Link from "next/link"; +import { useRouter } from "next/router"; // icons import { ChevronDown, Dot, XCircle } from "lucide-react"; +// ui +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { ConfirmProjectMemberRemove } from "components/project"; // constants -import { ROLE } from "constants/workspace"; -import { EUserProjectRoles } from "constants/project"; import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker"; +import { EUserProjectRoles } from "constants/project"; +import { ROLE } from "constants/workspace"; +// hooks +import { useEventTracker, useMember, useProject, useUser } from "hooks/store"; type Props = { userId: string; @@ -37,8 +36,6 @@ export const ProjectMemberListItem: React.FC = observer((props) => { project: { removeMemberFromProject, getProjectMemberDetails, updateMember }, } = useMember(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // derived values const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; @@ -47,7 +44,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { const handleRemove = async () => { if (!workspaceSlug || !projectId || !userDetails) return; - if (userDetails.member.id === currentUser?.id) { + if (userDetails.member?.id === currentUser?.id) { await leaveProject(workspaceSlug.toString(), projectId.toString()) .then(async () => { captureEvent(PROJECT_MEMBER_LEAVE, { @@ -58,17 +55,17 @@ export const ProjectMemberListItem: React.FC = observer((props) => { router.push(`/${workspaceSlug}/projects`); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) ); } else - await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), userDetails.member.id).catch( + await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), userDetails.member?.id).catch( (err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -87,12 +84,12 @@ export const ProjectMemberListItem: React.FC = observer((props) => { />
- {userDetails.member.avatar && userDetails.member.avatar !== "" ? ( - + {userDetails.member?.avatar && userDetails.member?.avatar !== "" ? ( + {userDetails.member.display_name @@ -100,23 +97,23 @@ export const ProjectMemberListItem: React.FC = observer((props) => { ) : ( - {(userDetails.member.display_name ?? userDetails.member.email ?? "?")[0]} + {(userDetails.member?.display_name ?? userDetails.member?.email ?? "?")[0]} )}
- + - {userDetails.member.first_name} {userDetails.member.last_name} + {userDetails.member?.first_name} {userDetails.member?.last_name}
-

{userDetails.member.display_name}

+

{userDetails.member?.display_name}

{isAdmin && ( <> -

{userDetails.member.email}

+

{userDetails.member?.email}

)}
@@ -129,12 +126,12 @@ export const ProjectMemberListItem: React.FC = observer((props) => {
{ROLE[userDetails.role]} - {userDetails.member.id !== currentUser?.id && ( + {userDetails.member?.id !== currentUser?.id && ( @@ -145,21 +142,21 @@ export const ProjectMemberListItem: React.FC = observer((props) => { onChange={(value: EUserProjectRoles) => { if (!workspaceSlug || !projectId) return; - updateMember(workspaceSlug.toString(), projectId.toString(), userDetails.member.id, { + updateMember(workspaceSlug.toString(), projectId.toString(), userDetails.member?.id, { role: value, }).catch((err) => { const error = err.error; const errorString = Array.isArray(error) ? error[0] : error; - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: errorString ?? "An error occurred while updating member role. Please try again.", }); }); }} disabled={ - userDetails.member.id === currentUser?.id || !currentProjectRole || currentProjectRole < userDetails.role + userDetails.member?.id === currentUser?.id || !currentProjectRole || currentProjectRole < userDetails.role } placement="bottom-end" > @@ -173,8 +170,8 @@ export const ProjectMemberListItem: React.FC = observer((props) => { ); })} - {(isAdmin || userDetails.member.id === currentUser?.id) && ( - + {(isAdmin || userDetails.member?.id === currentUser?.id) && ( +
} - className={`hidden flex-shrink-0 group-hover:block ${isMenuActive ? "!block" : ""}`} + className={cn("hidden flex-shrink-0 group-hover:block", { + "!block": isMenuActive, + })} buttonClassName="!text-custom-sidebar-text-400" ellipsis placement="bottom-start" diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 983e23932..2cee91e6b 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -1,20 +1,21 @@ -import { useState, FC, useRef, useEffect, useCallback } from "react"; -import { useRouter } from "next/router"; +import { useState, FC, useRef, useEffect } from "react"; import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; -import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import { Disclosure, Transition } from "@headlessui/react"; import { ChevronDown, ChevronRight, Plus } from "lucide-react"; // hooks -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components +import { TOAST_TYPE, setToast } from "@plane/ui"; import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; -// helpers -import { copyUrlToClipboard } from "helpers/string.helper"; -import { orderJoinedProjects } from "helpers/project.helper"; -import { cn } from "helpers/common.helper"; -// constants import { EUserWorkspaceRoles } from "constants/workspace"; +import { cn } from "helpers/common.helper"; +import { orderJoinedProjects } from "helpers/project.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +// ui +// components +// helpers +// constants import { IProject } from "@plane/types"; export const ProjectSidebarList: FC = observer(() => { @@ -42,15 +43,13 @@ export const ProjectSidebarList: FC = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast - const { setToastAlert } = useToast(); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const handleCopyText = (projectId: string) => { copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Project link copied to clipboard.", }); @@ -64,16 +63,16 @@ export const ProjectSidebarList: FC = observer(() => { const joinedProjectsList: IProject[] = []; joinedProjects.map((projectId) => { - const _project = getProjectById(projectId); - if (_project) joinedProjectsList.push(_project); + const projectDetails = getProjectById(projectId); + if (projectDetails) joinedProjectsList.push(projectDetails); }); if (joinedProjectsList.length <= 0) return; const updatedSortOrder = orderJoinedProjects(source.index, destination.index, draggableId, joinedProjectsList); if (updatedSortOrder != undefined) updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/components/states/create-state-modal.tsx b/web/components/states/create-state-modal.tsx index db91bb6b0..b142cc60e 100644 --- a/web/components/states/create-state-modal.tsx +++ b/web/components/states/create-state-modal.tsx @@ -1,20 +1,19 @@ import React from "react"; -import { useRouter } from "next/router"; -import { Controller, useForm } from "react-hook-form"; -import { TwitterPicker } from "react-color"; -import { Dialog, Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; -// hooks -import { useProjectState } from "hooks/store"; -import useToast from "hooks/use-toast"; -// ui -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; +import { useRouter } from "next/router"; +import { TwitterPicker } from "react-color"; +import { Controller, useForm } from "react-hook-form"; +import { Dialog, Popover, Transition } from "@headlessui/react"; // icons import { ChevronDown } from "lucide-react"; -// types -import type { IState } from "@plane/types"; +// ui +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { GROUP_CHOICES } from "constants/project"; +// hooks +import { useProjectState } from "hooks/store"; +// types +import type { IState } from "@plane/types"; // types type Props = { @@ -37,8 +36,6 @@ export const CreateStateModal: React.FC = observer((props) => { const { workspaceSlug } = router.query; // store hooks const { createState } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // form info const { formState: { errors, isSubmitting }, @@ -71,15 +68,15 @@ export const CreateStateModal: React.FC = observer((props) => { if (typeof error === "object") { Object.keys(error).forEach((key) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: Array.isArray(error[key]) ? error[key].join(", ") : error[key], }); }); } else { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error ?? err.status === 400 diff --git a/web/components/states/create-update-state-inline.tsx b/web/components/states/create-update-state-inline.tsx index 037cd483d..88c50a017 100644 --- a/web/components/states/create-update-state-inline.tsx +++ b/web/components/states/create-update-state-inline.tsx @@ -1,19 +1,18 @@ import React, { useEffect } from "react"; -import { useRouter } from "next/router"; -import { useForm, Controller } from "react-hook-form"; -import { TwitterPicker } from "react-color"; -import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import { TwitterPicker } from "react-color"; +import { useForm, Controller } from "react-hook-form"; +import { Popover, Transition } from "@headlessui/react"; +// ui +import { Button, CustomSelect, Input, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { STATE_CREATED, STATE_UPDATED } from "constants/event-tracker"; +import { GROUP_CHOICES } from "constants/project"; // hooks import { useEventTracker, useProjectState } from "hooks/store"; -import useToast from "hooks/use-toast"; -// ui -import { Button, CustomSelect, Input, Tooltip } from "@plane/ui"; // types import type { IState } from "@plane/types"; -// constants -import { GROUP_CHOICES } from "constants/project"; -import { STATE_CREATED, STATE_UPDATED } from "constants/event-tracker"; type Props = { data: IState | null; @@ -39,8 +38,6 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { // store hooks const { captureProjectStateEvent, setTrackElement } = useEventTracker(); const { createState, updateState } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -82,8 +79,8 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { await createState(workspaceSlug.toString(), projectId.toString(), formData) .then((res) => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "State created successfully.", }); @@ -98,14 +95,14 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { }) .catch((error) => { if (error.status === 400) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State with that name already exists. Please try again with another name.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State could not be created. Please try again.", }); @@ -135,22 +132,22 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { element: "Project settings states page", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "State updated successfully.", }); }) .catch((error) => { if (error.status === 400) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Another state exists with the same name. Please try again with another name.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State could not be updated. Please try again.", }); diff --git a/web/components/states/delete-state-modal.tsx b/web/components/states/delete-state-modal.tsx index 12de38608..7496b53b4 100644 --- a/web/components/states/delete-state-modal.tsx +++ b/web/components/states/delete-state-modal.tsx @@ -1,17 +1,16 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useEventTracker, useProjectState } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button } from "@plane/ui"; -// types -import type { IState } from "@plane/types"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { STATE_DELETED } from "constants/event-tracker"; +// hooks +import { useEventTracker, useProjectState } from "hooks/store"; +// types +import type { IState } from "@plane/types"; type Props = { isOpen: boolean; @@ -29,8 +28,6 @@ export const DeleteStateModal: React.FC = observer((props) => { // store hooks const { captureProjectStateEvent } = useEventTracker(); const { deleteState } = useProjectState(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -55,15 +52,15 @@ export const DeleteStateModal: React.FC = observer((props) => { }) .catch((err) => { if (err.status === 400) - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "This state contains some issues within it, please move them to some other state to delete this state.", }); else - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "State could not be deleted. Please try again.", }); diff --git a/web/components/states/project-setting-state-list-item.tsx b/web/components/states/project-setting-state-list-item.tsx index 401c482f3..760c8501c 100644 --- a/web/components/states/project-setting-state-list-item.tsx +++ b/web/components/states/project-setting-state-list-item.tsx @@ -1,14 +1,14 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useEventTracker, useProjectState } from "hooks/store"; // ui +import { Pencil, X, ArrowDown, ArrowUp } from "lucide-react"; import { Tooltip, StateGroupIcon } from "@plane/ui"; // icons -import { Pencil, X, ArrowDown, ArrowUp } from "lucide-react"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; +import { useEventTracker, useProjectState } from "hooks/store"; // types import { IState } from "@plane/types"; diff --git a/web/components/states/project-setting-state-list.tsx b/web/components/states/project-setting-state-list.tsx index 99ac40d84..5f7772567 100644 --- a/web/components/states/project-setting-state-list.tsx +++ b/web/components/states/project-setting-state-list.tsx @@ -1,20 +1,20 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks +import { Plus } from "lucide-react"; +import { Loader } from "@plane/ui"; +import { CreateUpdateStateInline, DeleteStateModal, StateGroup, StatesListItem } from "components/states"; +import { STATES_LIST } from "constants/fetch-keys"; +import { sortByField } from "helpers/array.helper"; +import { orderStateGroups } from "helpers/state.helper"; import { useEventTracker, useProjectState } from "hooks/store"; // components -import { CreateUpdateStateInline, DeleteStateModal, StateGroup, StatesListItem } from "components/states"; // ui -import { Loader } from "@plane/ui"; // icons -import { Plus } from "lucide-react"; // helpers -import { orderStateGroups } from "helpers/state.helper"; -import { sortByField } from "helpers/array.helper"; // fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; export const ProjectSettingStateList: React.FC = observer(() => { // router diff --git a/web/components/toast-alert/index.tsx b/web/components/toast-alert/index.tsx deleted file mode 100644 index b4df6ea05..000000000 --- a/web/components/toast-alert/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from "react"; -// hooks -import useToast from "hooks/use-toast"; -// icons -import { AlertTriangle, CheckCircle, Info, X, XCircle } from "lucide-react"; - -const ToastAlerts = () => { - const { alerts, removeAlert } = useToast(); - - if (!alerts) return null; - - return ( -
- {alerts.map((alert) => ( -
-
- -
-
-
-
- {alert.type === "success" ? ( -
-
-

{alert.title}

- {alert.message &&

{alert.message}

} -
-
-
-
- ))} -
- ); -}; - -export default ToastAlerts; diff --git a/web/components/ui/empty-space.tsx b/web/components/ui/empty-space.tsx index 4b70bbb15..73fc6ba01 100644 --- a/web/components/ui/empty-space.tsx +++ b/web/components/ui/empty-space.tsx @@ -1,7 +1,7 @@ // next +import React from "react"; import Link from "next/link"; // react -import React from "react"; // icons import { ChevronRight } from "lucide-react"; diff --git a/web/components/ui/graphs/bar-graph.tsx b/web/components/ui/graphs/bar-graph.tsx index 3756b0455..3f40aad87 100644 --- a/web/components/ui/graphs/bar-graph.tsx +++ b/web/components/ui/graphs/bar-graph.tsx @@ -1,11 +1,11 @@ // nivo import { ResponsiveBar, BarSvgProps } from "@nivo/bar"; // helpers +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { generateYAxisTickValues } from "helpers/graph.helper"; // types import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; type Props = { indexBy: string; diff --git a/web/components/ui/graphs/calendar-graph.tsx b/web/components/ui/graphs/calendar-graph.tsx index 0725c425a..a64a4a920 100644 --- a/web/components/ui/graphs/calendar-graph.tsx +++ b/web/components/ui/graphs/calendar-graph.tsx @@ -1,9 +1,9 @@ // nivo import { ResponsiveCalendar, CalendarSvgProps } from "@nivo/calendar"; // types +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; export const CalendarGraph: React.FC> = ({ height = "400px", diff --git a/web/components/ui/graphs/index.ts b/web/components/ui/graphs/index.ts index 1f40adbff..984bb642c 100644 --- a/web/components/ui/graphs/index.ts +++ b/web/components/ui/graphs/index.ts @@ -1,6 +1,5 @@ export * from "./bar-graph"; export * from "./calendar-graph"; export * from "./line-graph"; -export * from "./marimekko-graph"; export * from "./pie-graph"; export * from "./scatter-plot-graph"; diff --git a/web/components/ui/graphs/line-graph.tsx b/web/components/ui/graphs/line-graph.tsx index 91a19acc3..93eac0097 100644 --- a/web/components/ui/graphs/line-graph.tsx +++ b/web/components/ui/graphs/line-graph.tsx @@ -1,11 +1,11 @@ // nivo import { ResponsiveLine, LineSvgProps } from "@nivo/line"; // helpers +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { generateYAxisTickValues } from "helpers/graph.helper"; // types import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; type Props = { customYAxisTickValues?: number[]; diff --git a/web/components/ui/graphs/marimekko-graph.tsx b/web/components/ui/graphs/marimekko-graph.tsx deleted file mode 100644 index fd460d11b..000000000 --- a/web/components/ui/graphs/marimekko-graph.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// nivo -import { ResponsiveMarimekko, SvgProps } from "@nivo/marimekko"; -// helpers -import { generateYAxisTickValues } from "helpers/graph.helper"; -// types -import { TGraph } from "./types"; -// constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; - -type Props = { - id: string; - value: string; - customYAxisTickValues?: number[]; -}; - -export const MarimekkoGraph: React.FC, "height" | "width">> = ({ - id, - value, - customYAxisTickValues, - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
- 7 ? -45 : 0, - }} - labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }} - theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} - animate - {...rest} - /> -
-); diff --git a/web/components/ui/graphs/pie-graph.tsx b/web/components/ui/graphs/pie-graph.tsx index 52b56e492..739ede4b0 100644 --- a/web/components/ui/graphs/pie-graph.tsx +++ b/web/components/ui/graphs/pie-graph.tsx @@ -1,9 +1,9 @@ // nivo import { PieSvgProps, ResponsivePie } from "@nivo/pie"; // types +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; export const PieGraph: React.FC, "height" | "width">> = ({ height = "400px", diff --git a/web/components/ui/graphs/scatter-plot-graph.tsx b/web/components/ui/graphs/scatter-plot-graph.tsx index c6ff5a772..4eb82a97e 100644 --- a/web/components/ui/graphs/scatter-plot-graph.tsx +++ b/web/components/ui/graphs/scatter-plot-graph.tsx @@ -1,9 +1,9 @@ // nivo import { ResponsiveScatterPlot, ScatterPlotSvgProps } from "@nivo/scatterplot"; // types +import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { TGraph } from "./types"; // constants -import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; export const ScatterPlotGraph: React.FC, "height" | "width">> = ({ height = "400px", diff --git a/web/components/ui/loader/cycle-module-board-loader.tsx b/web/components/ui/loader/cycle-module-board-loader.tsx index 09c885fb9..f88719c38 100644 --- a/web/components/ui/loader/cycle-module-board-loader.tsx +++ b/web/components/ui/loader/cycle-module-board-loader.tsx @@ -2,8 +2,11 @@ export const CycleModuleBoardLayout = () => (
- {[...Array(5)].map(() => ( -
+ {[...Array(5)].map((i) => ( +
diff --git a/web/components/ui/loader/cycle-module-list-loader.tsx b/web/components/ui/loader/cycle-module-list-loader.tsx index 8787a1425..522b96f0d 100644 --- a/web/components/ui/loader/cycle-module-list-loader.tsx +++ b/web/components/ui/loader/cycle-module-list-loader.tsx @@ -2,8 +2,11 @@ export const CycleModuleListLayout = () => (
- {[...Array(5)].map(() => ( -
+ {[...Array(5)].map((i) => ( +
diff --git a/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx b/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx index 944ec02b8..3456e43ab 100644 --- a/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx +++ b/web/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx @@ -1,7 +1,7 @@ import React from "react"; // ui -import { InboxSidebarLoader } from "./inbox-sidebar-loader"; import { Loader } from "@plane/ui"; +import { InboxSidebarLoader } from "./inbox-sidebar-loader"; export const InboxLayoutLoader = () => (
diff --git a/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx b/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx index ce464e83d..204c2fff6 100644 --- a/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx +++ b/web/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader.tsx @@ -7,8 +7,8 @@ export const InboxSidebarLoader = () => (
- {[...Array(6)].map(() => ( -
+ {[...Array(6)].map((i) => ( +
diff --git a/web/components/ui/loader/notification-loader.tsx b/web/components/ui/loader/notification-loader.tsx index 143f1a9b6..7485c2c4c 100644 --- a/web/components/ui/loader/notification-loader.tsx +++ b/web/components/ui/loader/notification-loader.tsx @@ -1,7 +1,7 @@ export const NotificationsLoader = () => (
- {[...Array(3)].map(() => ( -
+ {[...Array(3)].map((i) => ( +
diff --git a/web/components/ui/loader/pages-loader.tsx b/web/components/ui/loader/pages-loader.tsx index e31e82942..612c17d88 100644 --- a/web/components/ui/loader/pages-loader.tsx +++ b/web/components/ui/loader/pages-loader.tsx @@ -4,13 +4,13 @@ export const PagesLoader = () => (

Pages

- {[...Array(5)].map(() => ( - + {[...Array(5)].map((i) => ( + ))}
- {[...Array(5)].map(() => ( -
+ {[...Array(5)].map((i) => ( +
diff --git a/web/components/ui/loader/projects-loader.tsx b/web/components/ui/loader/projects-loader.tsx index 9548a1f48..d1a781d6b 100644 --- a/web/components/ui/loader/projects-loader.tsx +++ b/web/components/ui/loader/projects-loader.tsx @@ -1,8 +1,11 @@ export const ProjectsLoader = () => (
- {[...Array(3)].map(() => ( -
+ {[...Array(3)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/activity.tsx b/web/components/ui/loader/settings/activity.tsx index 7bc5c392f..70297f644 100644 --- a/web/components/ui/loader/settings/activity.tsx +++ b/web/components/ui/loader/settings/activity.tsx @@ -2,8 +2,8 @@ import { getRandomLength } from "../utils"; export const ActivitySettingsLoader = () => (
- {[...Array(10)].map(() => ( -
+ {[...Array(10)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/api-token.tsx b/web/components/ui/loader/settings/api-token.tsx index fc5b4c41d..e31090bff 100644 --- a/web/components/ui/loader/settings/api-token.tsx +++ b/web/components/ui/loader/settings/api-token.tsx @@ -5,8 +5,8 @@ export const APITokenSettingsLoader = () => (
- {[...Array(2)].map(() => ( -
+ {[...Array(2)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/email.tsx b/web/components/ui/loader/settings/email.tsx index fa68b972f..87634bf09 100644 --- a/web/components/ui/loader/settings/email.tsx +++ b/web/components/ui/loader/settings/email.tsx @@ -8,8 +8,8 @@ export const EmailSettingsLoader = () => (
- {[...Array(4)].map(() => ( -
+ {[...Array(4)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/import-and-export.tsx b/web/components/ui/loader/settings/import-and-export.tsx index 70496d1c1..a3561207d 100644 --- a/web/components/ui/loader/settings/import-and-export.tsx +++ b/web/components/ui/loader/settings/import-and-export.tsx @@ -1,7 +1,7 @@ export const ImportExportSettingsLoader = () => (
- {[...Array(2)].map(() => ( -
+ {[...Array(2)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/integration.tsx b/web/components/ui/loader/settings/integration.tsx index 871b570b1..2260517ee 100644 --- a/web/components/ui/loader/settings/integration.tsx +++ b/web/components/ui/loader/settings/integration.tsx @@ -1,7 +1,10 @@ export const IntegrationsSettingsLoader = () => (
- {[...Array(2)].map(() => ( -
+ {[...Array(2)].map((i) => ( +
diff --git a/web/components/ui/loader/settings/members.tsx b/web/components/ui/loader/settings/members.tsx index 3ed2c41ef..e286320a9 100644 --- a/web/components/ui/loader/settings/members.tsx +++ b/web/components/ui/loader/settings/members.tsx @@ -1,7 +1,7 @@ export const MembersSettingsLoader = () => (
- {[...Array(4)].map(() => ( -
+ {[...Array(4)].map((i) => ( +
diff --git a/web/components/ui/loader/view-list-loader.tsx b/web/components/ui/loader/view-list-loader.tsx index 97899a657..8b59b57a2 100644 --- a/web/components/ui/loader/view-list-loader.tsx +++ b/web/components/ui/loader/view-list-loader.tsx @@ -1,7 +1,7 @@ export const ViewListLoader = () => (
- {[...Array(8)].map(() => ( -
+ {[...Array(8)].map((i) => ( +
diff --git a/web/components/ui/multi-level-dropdown.tsx b/web/components/ui/multi-level-dropdown.tsx index 7bf4aa8a1..d66702ccc 100644 --- a/web/components/ui/multi-level-dropdown.tsx +++ b/web/components/ui/multi-level-dropdown.tsx @@ -3,9 +3,9 @@ import { Fragment, useState } from "react"; // headless ui import { Menu, Transition } from "@headlessui/react"; // ui +import { Check, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; import { Loader } from "@plane/ui"; // icons -import { Check, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; type MultiLevelDropdownProps = { label: string; @@ -73,8 +73,8 @@ export const MultiLevelDropdown: React.FC = ({ as="button" onClick={(e: any) => { if (option.hasChildren) { - e.stopPropagation(); - e.preventDefault(); + e?.stopPropagation(); + e?.preventDefault(); if (option.onClick) option.onClick(); diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index 5bd477352..180c293e0 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -1,13 +1,12 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; +// ui +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // hooks import { useProjectView } from "hooks/store"; -import useToast from "hooks/use-toast"; -// ui -import { Button } from "@plane/ui"; // types import { IProjectView } from "@plane/types"; @@ -26,8 +25,6 @@ export const DeleteProjectViewModal: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { deleteView } = useProjectView(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -43,15 +40,15 @@ export const DeleteProjectViewModal: React.FC = observer((props) => { .then(() => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View deleted successfully.", }); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be deleted. Please try again.", }) diff --git a/web/components/views/form.tsx b/web/components/views/form.tsx index 0da7e3946..31fee1006 100644 --- a/web/components/views/form.tsx +++ b/web/components/views/form.tsx @@ -2,15 +2,15 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; // hooks +import { Button, Input, TextArea } from "@plane/ui"; +import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { useLabel, useMember, useProjectState } from "hooks/store"; // components -import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; // ui -import { Button, Input, TextArea } from "@plane/ui"; // types import { IProjectView, IIssueFilterOptions } from "@plane/types"; // constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; type Props = { data?: IProjectView | null; @@ -212,8 +212,8 @@ export const ProjectViewForm: React.FC = observer((props) => { ? "Updating View..." : "Update View" : isSubmitting - ? "Creating View..." - : "Create View"} + ? "Creating View..." + : "Create View"}
diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index 43cea7d5c..7e0c92f26 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -1,11 +1,12 @@ import { FC, Fragment } from "react"; import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// hooks -import { useProjectView } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ProjectViewForm } from "components/views"; +// hooks +import { useProjectView } from "hooks/store"; // types import { IProjectView } from "@plane/types"; @@ -22,8 +23,6 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { const { data, isOpen, onClose, preLoadedData, workspaceSlug, projectId } = props; // store hooks const { createView, updateView } = useProjectView(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -33,15 +32,15 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { await createView(workspaceSlug, projectId, payload) .then(() => { handleClose(); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View created successfully.", }); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }) @@ -52,8 +51,8 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { await updateView(workspaceSlug, projectId, data?.id as string, payload) .then(() => handleClose()) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err.detail ?? "Something went wrong. Please try again.", }) diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index 48cc12ada..29d5bac57 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -1,22 +1,21 @@ import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "lucide-react"; -// hooks -import { useProjectView, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; -// ui -import { CustomMenu } from "@plane/ui"; +// constants +import { EUserProjectRoles } from "constants/project"; // helpers import { calculateTotalFilters } from "helpers/filter.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useProjectView, useUser } from "hooks/store"; // types import { IProjectView } from "@plane/types"; -// constants -import { EUserProjectRoles } from "constants/project"; type Props = { view: IProjectView; @@ -30,8 +29,6 @@ export const ProjectViewListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -54,8 +51,8 @@ export const ProjectViewListItem: React.FC = observer((props) => { e.stopPropagation(); e.preventDefault(); copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/views/${view.id}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "View link copied to clipboard.", }); diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index 4977ed3e7..ba4bef2b8 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -1,45 +1,32 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search } from "lucide-react"; -import { useTheme } from "next-themes"; // hooks -import { useApplication, useProjectView, useUser } from "hooks/store"; +import { useApplication, useProjectView } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; +import { ViewListLoader } from "components/ui"; import { ProjectViewListItem } from "components/views"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui import { Input } from "@plane/ui"; -import { ViewListLoader } from "components/ui"; // constants -import { EUserProjectRoles } from "constants/project"; -import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; export const ProjectViewsList = observer(() => { // states const [query, setQuery] = useState(""); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateViewModal }, } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { projectViewIds, getViewById, loader } = useProjectView(); if (loader || !projectViewIds) return ; const viewsList = projectViewIds.map((viewId) => getViewById(viewId)); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode); - const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase())); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - return ( <> {viewsList.length > 0 ? ( @@ -65,21 +52,7 @@ export const ProjectViewsList = observer(() => {
) : ( - toggleCreateViewModal(true), - }} - size="lg" - disabled={!isEditingAllowed} - /> + toggleCreateViewModal(true)} /> )} ); diff --git a/web/components/web-hooks/create-webhook-modal.tsx b/web/components/web-hooks/create-webhook-modal.tsx index f8301bf53..b18beede8 100644 --- a/web/components/web-hooks/create-webhook-modal.tsx +++ b/web/components/web-hooks/create-webhook-modal.tsx @@ -1,17 +1,18 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; +// ui import { Dialog, Transition } from "@headlessui/react"; +import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { WebhookForm } from "./form"; -import { GeneratedHookDetails } from "./generated-hook-details"; -// hooks -import useToast from "hooks/use-toast"; // helpers import { csvDownload } from "helpers/download.helper"; -// utils -import { getCurrentHookAsCSV } from "./utils"; // types import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types"; +import { WebhookForm } from "./form"; +import { GeneratedHookDetails } from "./generated-hook-details"; +// utils +import { getCurrentHookAsCSV } from "./utils"; +// ui interface ICreateWebhookModal { currentWorkspace: IWorkspace | null; @@ -34,8 +35,6 @@ export const CreateWebhookModal: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // toast - const { setToastAlert } = useToast(); const handleCreateWebhook = async (formData: IWebhook, webhookEventType: TWebhookEventTypes) => { if (!workspaceSlug) return; @@ -65,8 +64,8 @@ export const CreateWebhookModal: React.FC = (props) => { await createWebhook(workspaceSlug.toString(), payload) .then(({ webHook, secretKey }) => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Webhook created successfully.", }); @@ -77,8 +76,8 @@ export const CreateWebhookModal: React.FC = (props) => { csvDownload(csvData, `webhook-secret-key-${Date.now()}`); }) .catch((error) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/components/web-hooks/delete-webhook-modal.tsx b/web/components/web-hooks/delete-webhook-modal.tsx index 6cc30bb57..22f8aca32 100644 --- a/web/components/web-hooks/delete-webhook-modal.tsx +++ b/web/components/web-hooks/delete-webhook-modal.tsx @@ -2,11 +2,10 @@ import React, { FC, useState } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; +// ui +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // hooks import { useWebhook } from "hooks/store"; -import useToast from "hooks/use-toast"; -// ui -import { Button } from "@plane/ui"; interface IDeleteWebhook { isOpen: boolean; @@ -19,8 +18,6 @@ export const DeleteWebhookModal: FC = (props) => { const [isDeleting, setIsDeleting] = useState(false); // router const router = useRouter(); - // toast - const { setToastAlert } = useToast(); // store hooks const { removeWebhook } = useWebhook(); @@ -37,16 +34,16 @@ export const DeleteWebhookModal: FC = (props) => { removeWebhook(workspaceSlug.toString(), webhookId.toString()) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Webhook deleted successfully.", }); router.replace(`/${workspaceSlug}/settings/webhooks/`); }) .catch((error) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/web-hooks/form/form.tsx b/web/components/web-hooks/form/form.tsx index c2dd940dc..1b1e1bf27 100644 --- a/web/components/web-hooks/form/form.tsx +++ b/web/components/web-hooks/form/form.tsx @@ -1,9 +1,8 @@ import React, { FC, useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; // hooks -import { useWebhook } from "hooks/store"; -// components +import { Button } from "@plane/ui"; import { WebhookIndividualEventOptions, WebhookInput, @@ -11,8 +10,9 @@ import { WebhookSecretKey, WebhookToggle, } from "components/web-hooks"; +import { useWebhook } from "hooks/store"; +// components // ui -import { Button } from "@plane/ui"; // types import { IWebhook, TWebhookEventTypes } from "@plane/types"; @@ -36,7 +36,7 @@ export const WebhookForm: FC = observer((props) => { // states const [webhookEventType, setWebhookEventType] = useState("all"); // store hooks - const {webhookSecretKey } = useWebhook(); + const { webhookSecretKey } = useWebhook(); // use form const { handleSubmit, diff --git a/web/components/web-hooks/form/secret-key.tsx b/web/components/web-hooks/form/secret-key.tsx index 2d6a69fd6..11129fb07 100644 --- a/web/components/web-hooks/form/secret-key.tsx +++ b/web/components/web-hooks/form/secret-key.tsx @@ -1,18 +1,19 @@ import { useState, FC } from "react"; -import { useRouter } from "next/router"; -import { Button, Tooltip } from "@plane/ui"; -import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +// icons +import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react"; +// ui +import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +// helpers +import { csvDownload } from "helpers/download.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; // hooks import { useWebhook, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; -// helpers -import { copyTextToClipboard } from "helpers/string.helper"; -import { csvDownload } from "helpers/download.helper"; -// utils -import { getCurrentHookAsCSV } from "../utils"; // types import { IWebhook } from "@plane/types"; +// utils +import { getCurrentHookAsCSV } from "../utils"; type Props = { data: Partial; @@ -29,23 +30,21 @@ export const WebhookSecretKey: FC = observer((props) => { // store hooks const { currentWorkspace } = useWorkspace(); const { currentWebhook, regenerateSecretKey, webhookSecretKey } = useWebhook(); - // hooks - const { setToastAlert } = useToast(); const handleCopySecretKey = () => { if (!webhookSecretKey) return; copyTextToClipboard(webhookSecretKey) .then(() => - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Secret key copied to clipboard.", }) ) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Error occurred while copying secret key.", }) @@ -59,8 +58,8 @@ export const WebhookSecretKey: FC = observer((props) => { regenerateSecretKey(workspaceSlug.toString(), data.id) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "New key regenerated successfully.", }); @@ -71,8 +70,8 @@ export const WebhookSecretKey: FC = observer((props) => { } }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) diff --git a/web/components/web-hooks/generated-hook-details.tsx b/web/components/web-hooks/generated-hook-details.tsx index ce78fa5d5..2cd5ef986 100644 --- a/web/components/web-hooks/generated-hook-details.tsx +++ b/web/components/web-hooks/generated-hook-details.tsx @@ -1,9 +1,9 @@ // components -import { WebhookSecretKey } from "./form"; // ui import { Button } from "@plane/ui"; // types import { IWebhook } from "@plane/types"; +import { WebhookSecretKey } from "./form"; type Props = { handleClose: () => void; diff --git a/web/components/web-hooks/webhooks-list-item.tsx b/web/components/web-hooks/webhooks-list-item.tsx index fa676fccd..2f9ca52a5 100644 --- a/web/components/web-hooks/webhooks-list-item.tsx +++ b/web/components/web-hooks/webhooks-list-item.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; // hooks +import { ToggleSwitch } from "@plane/ui"; import { useWebhook } from "hooks/store"; // ui -import { ToggleSwitch } from "@plane/ui"; // types import { IWebhook } from "@plane/types"; diff --git a/web/components/workspace/confirm-workspace-member-remove.tsx b/web/components/workspace/confirm-workspace-member-remove.tsx index 6c5ec4593..4c6b85573 100644 --- a/web/components/workspace/confirm-workspace-member-remove.tsx +++ b/web/components/workspace/confirm-workspace-member-remove.tsx @@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; // hooks +import { Button } from "@plane/ui"; import { useUser } from "hooks/store"; // ui -import { Button } from "@plane/ui"; type Props = { isOpen: boolean; @@ -73,7 +73,7 @@ export const ConfirmWorkspaceMemberRemove: React.FC = observer((props) => {currentUser?.id === userDetails.id ? "Leave workspace?" - : `Remove ${userDetails.display_name}?`} + : `Remove ${userDetails?.display_name}?`}
{currentUser?.id === userDetails.id ? ( @@ -84,7 +84,7 @@ export const ConfirmWorkspaceMemberRemove: React.FC = observer((props) => ) : (

Are you sure you want to remove member-{" "} - {userDetails.display_name}? They will no longer have + {userDetails?.display_name}? They will no longer have access to this workspace. This action cannot be undone.

)} @@ -102,8 +102,8 @@ export const ConfirmWorkspaceMemberRemove: React.FC = observer((props) => ? "Leaving" : "Leave" : isRemoving - ? "Removing" - : "Remove"} + ? "Removing" + : "Remove"}
diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index b4f164469..9cbfa25a3 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -1,19 +1,18 @@ import { Dispatch, SetStateAction, useEffect, useState, FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; -// services -import { WorkspaceService } from "services/workspace.service"; +// ui +import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { WORKSPACE_CREATED } from "constants/event-tracker"; +import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace"; // hooks import { useEventTracker, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input } from "@plane/ui"; // types import { IWorkspace } from "@plane/types"; -// constants -import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace"; -import { WORKSPACE_CREATED } from "constants/event-tracker"; +import { WorkspaceService } from "services/workspace.service"; type Props = { onSubmit?: (res: IWorkspace) => Promise; @@ -22,7 +21,7 @@ type Props = { slug: string; organization_size: string; }; - setDefaultValues: Dispatch>; + setDefaultValues: Dispatch>; secondaryButton?: React.ReactNode; primaryButtonText?: { loading: string; @@ -51,8 +50,6 @@ export const CreateWorkspaceForm: FC = observer((props) => { // store hooks const { captureWorkspaceEvent } = useEventTracker(); const { createWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -79,8 +76,8 @@ export const CreateWorkspaceForm: FC = observer((props) => { element: "Create workspace page", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace created successfully.", }); @@ -95,8 +92,8 @@ export const CreateWorkspaceForm: FC = observer((props) => { element: "Create workspace page", }, }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Workspace could not be created. Please try again.", }); @@ -104,8 +101,8 @@ export const CreateWorkspaceForm: FC = observer((props) => { } else setSlugError(true); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Some error occurred while creating workspace. Please try again.", }); diff --git a/web/components/workspace/delete-workspace-modal.tsx b/web/components/workspace/delete-workspace-modal.tsx index a90ac9cdf..0691fbbf0 100644 --- a/web/components/workspace/delete-workspace-modal.tsx +++ b/web/components/workspace/delete-workspace-modal.tsx @@ -1,18 +1,17 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// hooks -import { useEventTracker, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Button, Input } from "@plane/ui"; -// types -import type { IWorkspace } from "@plane/types"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { WORKSPACE_DELETED } from "constants/event-tracker"; +// hooks +import { useEventTracker, useWorkspace } from "hooks/store"; +// types +import type { IWorkspace } from "@plane/types"; type Props = { isOpen: boolean; @@ -32,8 +31,6 @@ export const DeleteWorkspaceModal: React.FC = observer((props) => { // store hooks const { captureWorkspaceEvent } = useEventTracker(); const { deleteWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); // form info const { control, @@ -58,7 +55,7 @@ export const DeleteWorkspaceModal: React.FC = observer((props) => { if (!data || !canDelete) return; await deleteWorkspace(data.slug) - .then((res) => { + .then(() => { handleClose(); router.push("/"); captureWorkspaceEvent({ @@ -69,15 +66,15 @@ export const DeleteWorkspaceModal: React.FC = observer((props) => { element: "Workspace general settings page", }, }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace deleted successfully.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again later.", }); diff --git a/web/components/workspace/help-section.tsx b/web/components/workspace/help-section.tsx index a61ac1823..210bbbd3a 100644 --- a/web/components/workspace/help-section.tsx +++ b/web/components/workspace/help-section.tsx @@ -1,18 +1,19 @@ import React, { useRef, useState } from "react"; -import Link from "next/link"; -import { Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +// headless ui +import { Transition } from "@headlessui/react"; +// icons +import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react"; +// ui +import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; // hooks import { useApplication } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// icons -import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react"; -import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; // assets import packageJson from "package.json"; -import useSize from "hooks/use-window-size"; -const helpOptions = [ +const HELP_OPTIONS = [ { name: "Documentation", href: "https://docs.plane.so/", @@ -28,12 +29,6 @@ const helpOptions = [ href: "https://github.com/makeplane/plane/issues/new/choose", Icon: GithubIcon, }, - { - name: "Chat with us", - href: null, - onClick: () => (window as any).$crisp.push(["do", "chat:show"]), - Icon: MessagesSquare, - }, ]; export interface WorkspaceHelpSectionProps { @@ -43,16 +38,20 @@ export interface WorkspaceHelpSectionProps { export const WorkspaceHelpSection: React.FC = observer(() => { // store hooks const { - theme: { sidebarCollapsed, toggleSidebar, toggleMobileSidebar }, + theme: { sidebarCollapsed, toggleSidebar }, commandPalette: { toggleShortcutModal }, } = useApplication(); - - const [windowWidth] = useSize(); // states const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); // refs const helpOptionsRef = useRef(null); + const handleCrispWindowShow = () => { + if (window) { + window.$crisp.push(["do", "chat:show"]); + } + }; + useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false)); const isCollapsed = sidebarCollapsed || false; @@ -60,8 +59,9 @@ export const WorkspaceHelpSection: React.FC = observe return ( <>
{!isCollapsed && (
@@ -72,8 +72,9 @@ export const WorkspaceHelpSection: React.FC = observe @@ -101,9 +103,10 @@ export const WorkspaceHelpSection: React.FC = observe @@ -121,38 +124,32 @@ export const WorkspaceHelpSection: React.FC = observe leaveTo="transform opacity-0 scale-95" >
- {helpOptions.map(({ name, Icon, href, onClick }) => { - if (href) - return ( - - -
- -
- {name} -
- - ); - else - return ( - - ); - })} + {HELP_OPTIONS.map(({ name, Icon, href }) => ( + + +
+ +
+ {name} +
+ + ))} +
Version: v{packageJson.version}
diff --git a/web/components/workspace/send-workspace-invitation-modal.tsx b/web/components/workspace/send-workspace-invitation-modal.tsx index 35b5963d0..55f64bfab 100644 --- a/web/components/workspace/send-workspace-invitation-modal.tsx +++ b/web/components/workspace/send-workspace-invitation-modal.tsx @@ -3,14 +3,14 @@ import { observer } from "mobx-react-lite"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; import { Plus, X } from "lucide-react"; -// hooks -import { useUser } from "hooks/store"; // ui import { Button, CustomSelect, Input } from "@plane/ui"; -// types -import { IWorkspaceBulkInviteFormData } from "@plane/types"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +// hooks +import { useUser } from "hooks/store"; +// types +import { IWorkspaceBulkInviteFormData } from "@plane/types"; type Props = { isOpen: boolean; @@ -121,7 +121,7 @@ export const SendWorkspaceInvitationModal: React.FC = observer((props) =>
{fields.map((field, index) => ( -
+
= observer((props) => )} />
-
+
= observer((props) => label={{ROLE[value]}} onChange={onChange} optionsClassName="w-full" + className="flex-grow" input > {Object.entries(ROLE).map(([key, value]) => { @@ -179,16 +180,16 @@ export const SendWorkspaceInvitationModal: React.FC = observer((props) => )} /> + {fields.length > 1 && ( + + )}
- {fields.length > 1 && ( - - )}
))}
diff --git a/web/components/workspace/settings/invitations-list-item.tsx b/web/components/workspace/settings/invitations-list-item.tsx index 9e37a2fb5..8c6de24b2 100644 --- a/web/components/workspace/settings/invitations-list-item.tsx +++ b/web/components/workspace/settings/invitations-list-item.tsx @@ -1,16 +1,15 @@ import { useState, FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { ChevronDown, XCircle } from "lucide-react"; -// hooks -import { useMember, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ConfirmWorkspaceMemberRemove } from "components/workspace"; -// ui -import { CustomSelect, Tooltip } from "@plane/ui"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +// hooks +import { useMember, useUser } from "hooks/store"; type Props = { invitationId: string; @@ -30,8 +29,6 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { const { workspace: { updateMemberInvitation, deleteMemberInvitation, getWorkspaceInvitationDetails }, } = useMember(); - // toast alert - const { setToastAlert } = useToast(); // derived values const invitationDetails = getWorkspaceInvitationDetails(invitationId); @@ -40,15 +37,15 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success", message: "Invitation removed successfully.", }); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -116,8 +113,8 @@ export const WorkspaceInvitationsListItem: FC = observer((props) => { updateMemberInvitation(workspaceSlug.toString(), invitationDetails.id, { role: value, }).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "An error occurred while updating member role. Please try again.", }); diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index 76c9bbedf..f40d78bb0 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -1,18 +1,18 @@ import { useState, FC } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; +// lucide icons import { ChevronDown, Dot, XCircle } from "lucide-react"; -// hooks -import { useEventTracker, useMember, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // components import { ConfirmWorkspaceMemberRemove } from "components/workspace"; -// ui -import { CustomSelect, Tooltip } from "@plane/ui"; // constants -import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; import { WORKSPACE_MEMBER_lEAVE } from "constants/event-tracker"; +import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +// hooks +import { useEventTracker, useMember, useUser } from "hooks/store"; type Props = { memberId: string; @@ -35,8 +35,6 @@ export const WorkspaceMembersListItem: FC = observer((props) => { workspace: { updateMember, removeMemberFromWorkspace, getWorkspaceMemberDetails }, } = useMember(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); // derived values const memberDetails = getWorkspaceMemberDetails(memberId); @@ -52,8 +50,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { router.push("/profile"); }) .catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -64,8 +62,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { if (!workspaceSlug || !memberDetails) return; await removeMemberFromWorkspace(workspaceSlug.toString(), memberDetails.member.id).catch((err) => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error", message: err?.error || "Something went wrong. Please try again.", }) @@ -165,8 +163,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { updateMember(workspaceSlug.toString(), memberDetails.member.id, { role: value, }).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "An error occurred while updating member role. Please try again.", }); diff --git a/web/components/workspace/settings/members-list.tsx b/web/components/workspace/settings/members-list.tsx index 1dc02d508..216122525 100644 --- a/web/components/workspace/settings/members-list.tsx +++ b/web/components/workspace/settings/members-list.tsx @@ -1,13 +1,12 @@ import { FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; +// components +import { MembersSettingsLoader } from "components/ui"; +import { WorkspaceInvitationsListItem, WorkspaceMembersListItem } from "components/workspace"; // hooks import { useMember } from "hooks/store"; -// components -import { WorkspaceInvitationsListItem, WorkspaceMembersListItem } from "components/workspace"; -// ui -import { MembersSettingsLoader } from "components/ui"; export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer((props) => { const { searchQuery } = props; diff --git a/web/components/workspace/settings/workspace-details.tsx b/web/components/workspace/settings/workspace-details.tsx index 44da4291f..bfd1473ea 100644 --- a/web/components/workspace/settings/workspace-details.tsx +++ b/web/components/workspace/settings/workspace-details.tsx @@ -3,23 +3,22 @@ import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { Disclosure, Transition } from "@headlessui/react"; import { ChevronDown, ChevronUp, Pencil } from "lucide-react"; -// services -import { FileService } from "services/file.service"; -// hooks -import { useEventTracker, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; -// components -import { DeleteWorkspaceModal } from "components/workspace"; -import { WorkspaceImageUploadModal } from "components/core"; // ui -import { Button, CustomSelect, Input, Spinner } from "@plane/ui"; +import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { WorkspaceImageUploadModal } from "components/core"; +import { DeleteWorkspaceModal } from "components/workspace"; +// constants +import { WORKSPACE_UPDATED } from "constants/event-tracker"; +import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; +// hooks +import { useEventTracker, useUser, useWorkspace } from "hooks/store"; +// services +import { FileService } from "services/file.service"; // types import { IWorkspace } from "@plane/types"; -// constants -import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace"; -import { WORKSPACE_UPDATED } from "constants/event-tracker"; const defaultValues: Partial = { name: "", @@ -43,8 +42,6 @@ export const WorkspaceDetails: FC = observer(() => { membership: { currentWorkspaceRole }, } = useUser(); const { currentWorkspace, updateWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); // form info const { handleSubmit, @@ -77,9 +74,9 @@ export const WorkspaceDetails: FC = observer(() => { element: "Workspace general settings page", }, }); - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "Workspace updated successfully", }); }) @@ -110,16 +107,16 @@ export const WorkspaceDetails: FC = observer(() => { fileService.deleteFile(currentWorkspace.id, url).then(() => { updateWorkspace(currentWorkspace.slug, { logo: "" }) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Workspace picture removed successfully.", }); setIsImageUploadModalOpen(false); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "There was some error in deleting your profile picture. Please try again.", }); @@ -132,8 +129,8 @@ export const WorkspaceDetails: FC = observer(() => { if (!currentWorkspace) return; copyUrlToClipboard(`${currentWorkspace.slug}`).then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Workspace URL copied to the clipboard.", }); }); diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index aeb0a34c2..5d1695b33 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -1,18 +1,18 @@ import { Fragment, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; -import { Menu, Transition } from "@headlessui/react"; -import { mutate } from "swr"; -import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react"; import { usePopper } from "react-popper"; -// hooks -import { useApplication, useEventTracker, useUser, useWorkspace } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; +import { mutate } from "swr"; // ui -import { Avatar, Loader } from "@plane/ui"; +import { Menu, Transition } from "@headlessui/react"; +// icons +import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react"; +// plane ui +import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui"; +// hooks +import { useApplication, useUser, useWorkspace } from "hooks/store"; // types import { IWorkspace } from "@plane/types"; // Static Data @@ -54,13 +54,10 @@ export const WorkspaceSidebarDropdown = observer(() => { const { workspaceSlug } = router.query; // store hooks const { - theme: { sidebarCollapsed, toggleMobileSidebar }, + theme: { sidebarCollapsed, toggleSidebar }, } = useApplication(); - const { setTrackElement } = useEventTracker(); const { currentUser, updateCurrentUser, isUserInstanceAdmin, signOut } = useUser(); const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace(); - // hooks - const { setToastAlert } = useToast(); const { setTheme } = useTheme(); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); @@ -89,8 +86,8 @@ export const WorkspaceSidebarDropdown = observer(() => { router.push("/"); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) @@ -98,7 +95,7 @@ export const WorkspaceSidebarDropdown = observer(() => { }; const handleItemClick = () => { if (window.innerWidth < 768) { - toggleMobileSidebar(); + toggleSidebar(); } }; const workspacesList = Object.values(workspaces ?? {}); diff --git a/web/components/workspace/sidebar-menu.tsx b/web/components/workspace/sidebar-menu.tsx index 9f3f5e1d6..2069d8f27 100644 --- a/web/components/workspace/sidebar-menu.tsx +++ b/web/components/workspace/sidebar-menu.tsx @@ -1,20 +1,20 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -// hooks -import { useApplication, useEventTracker, useUser } from "hooks/store"; -// components -import { NotificationPopover } from "components/notifications"; +import { Crown } from "lucide-react"; // ui import { Tooltip } from "@plane/ui"; -import { Crown } from "lucide-react"; +// components +import { NotificationPopover } from "components/notifications"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; import { SIDEBAR_MENU_ITEMS } from "constants/dashboard"; import { SIDEBAR_CLICKED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; // helper import { cn } from "helpers/common.helper"; +// hooks +import { useApplication, useEventTracker, useUser } from "hooks/store"; export const WorkspaceSidebarMenu = observer(() => { // store hooks @@ -31,7 +31,7 @@ export const WorkspaceSidebarMenu = observer(() => { const handleLinkClick = (itemKey: string) => { if (window.innerWidth < 768) { - themeStore.toggleMobileSidebar(); + themeStore.toggleSidebar(); } captureEvent(SIDEBAR_CLICKED, { destination: itemKey, @@ -52,10 +52,11 @@ export const WorkspaceSidebarMenu = observer(() => { disabled={!themeStore?.sidebarCollapsed} >
{ { //useState control for displaying draft issue button instead of group hover const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const timeoutRef = useRef(); const isSidebarCollapsed = themeStore.sidebarCollapsed; @@ -41,7 +42,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { const disabled = joinedProjectIds.length === 0; const onMouseEnter = () => { - //if renet before timout clear the timeout + // if enter before time out clear the timeout timeoutRef?.current && clearTimeout(timeoutRef.current); setIsDraftButtonOpen(true); }; @@ -58,6 +59,7 @@ export const WorkspaceSidebarQuickAction = observer(() => { const draftIssues = storedValue ?? {}; if (workspaceSlug && draftIssues[workspaceSlug]) delete draftIssues[workspaceSlug]; setValue(draftIssues); + return Promise.resolve(); }; return ( @@ -66,8 +68,8 @@ export const WorkspaceSidebarQuickAction = observer(() => { isOpen={isDraftIssueModalOpen} onClose={() => setIsDraftIssueModalOpen(false)} data={workspaceDraftIssue ?? {}} - // storeType={storeType} - isDraft={true} + onSubmit={() => removeWorkspaceDraftIssue()} + isDraft />
= observer((props) => { // store hooks const { deleteGlobalView } = useGlobalView(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -53,8 +53,8 @@ export const DeleteGlobalViewModal: React.FC = observer((props) => { view_id: data.id, state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while deleting the view. Please try again.", }); diff --git a/web/components/workspace/views/form.tsx b/web/components/workspace/views/form.tsx index 71627c08a..04c88767a 100644 --- a/web/components/workspace/views/form.tsx +++ b/web/components/workspace/views/form.tsx @@ -1,16 +1,16 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; -// hooks -import { useLabel, useMember } from "hooks/store"; -// components -import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; // ui import { Button, Input, TextArea } from "@plane/ui"; -// types -import { IIssueFilterOptions, IWorkspaceView } from "@plane/types"; +// components +import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +// hooks +import { useLabel, useMember } from "hooks/store"; +// types +import { IIssueFilterOptions, IWorkspaceView } from "@plane/types"; type Props = { handleFormSubmit: (values: Partial) => Promise; @@ -200,8 +200,8 @@ export const WorkspaceViewForm: React.FC = observer((props) => { ? "Updating View..." : "Update View" : isSubmitting - ? "Creating View..." - : "Create View"} + ? "Creating View..." + : "Create View"}
diff --git a/web/components/workspace/views/header.tsx b/web/components/workspace/views/header.tsx index 223fda13c..97982e61e 100644 --- a/web/components/workspace/views/header.tsx +++ b/web/components/workspace/views/header.tsx @@ -1,15 +1,16 @@ import React, { useEffect, useRef, useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +// icons import { Plus } from "lucide-react"; -// store hooks -import { useEventTracker, useGlobalView, useUser } from "hooks/store"; // components import { CreateUpdateWorkspaceViewModal } from "components/workspace"; // constants -import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; import { GLOBAL_VIEW_OPENED } from "constants/event-tracker"; +import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; +// store hooks +import { useEventTracker, useGlobalView, useUser } from "hooks/store"; const ViewTab = observer((props: { viewId: string }) => { const { viewId } = props; @@ -69,7 +70,7 @@ export const GlobalViewsHeader: React.FC = observer(() => { activeTabElement.scrollIntoView({ behavior: "smooth", inline: diff > 500 ? "center" : "nearest" }); } } - }, [globalViewId, currentWorkspaceViews, containerRef]); + }, [globalViewId, currentWorkspaceViews, containerRef, captureEvent]); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; @@ -95,9 +96,7 @@ export const GlobalViewsHeader: React.FC = observer(() => { ))} - {currentWorkspaceViews?.map((viewId) => ( - - ))} + {currentWorkspaceViews?.map((viewId) => )}
{isAuthorizedUser && ( diff --git a/web/components/workspace/views/modal.tsx b/web/components/workspace/views/modal.tsx index b66d555fa..975018f16 100644 --- a/web/components/workspace/views/modal.tsx +++ b/web/components/workspace/views/modal.tsx @@ -1,16 +1,17 @@ import React from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -// store hooks -import { useEventTracker, useGlobalView } from "hooks/store"; -import useToast from "hooks/use-toast"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { WorkspaceViewForm } from "components/workspace"; -// types -import { IWorkspaceView } from "@plane/types"; // constants import { GLOBAL_VIEW_CREATED, GLOBAL_VIEW_UPDATED } from "constants/event-tracker"; +// store hooks +import { useEventTracker, useGlobalView } from "hooks/store"; +// types +import { IWorkspaceView } from "@plane/types"; type Props = { data?: IWorkspaceView; @@ -27,8 +28,6 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) // store hooks const { createGlobalView, updateGlobalView } = useGlobalView(); const { captureEvent } = useEventTracker(); - // toast alert - const { setToastAlert } = useToast(); const handleClose = () => { onClose(); @@ -51,8 +50,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: res.filters, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View created successfully.", }); @@ -65,8 +64,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: payload?.filters, state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be created. Please try again.", }); @@ -90,8 +89,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: res.filters, state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View updated successfully.", }); @@ -103,8 +102,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) applied_filters: data.filters, state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be updated. Please try again.", }); diff --git a/web/components/workspace/views/view-list-item.tsx b/web/components/workspace/views/view-list-item.tsx index 28f25551c..4030dc181 100644 --- a/web/components/workspace/views/view-list-item.tsx +++ b/web/components/workspace/views/view-list-item.tsx @@ -1,17 +1,18 @@ import { useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useRouter } from "next/router"; +// icons import { Pencil, Trash2 } from "lucide-react"; -// store hooks -import { useEventTracker, useGlobalView } from "hooks/store"; -// components -import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; // ui import { CustomMenu } from "@plane/ui"; +// components +import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; // helpers -import { truncateText } from "helpers/string.helper"; import { calculateTotalFilters } from "helpers/filter.helper"; +import { truncateText } from "helpers/string.helper"; +// store hooks +import { useEventTracker, useGlobalView } from "hooks/store"; type Props = { viewId: string }; diff --git a/web/components/workspace/views/views-list.tsx b/web/components/workspace/views/views-list.tsx index 9a8758d2d..ef33fe16e 100644 --- a/web/components/workspace/views/views-list.tsx +++ b/web/components/workspace/views/views-list.tsx @@ -1,12 +1,11 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; +// components +import { ViewListLoader } from "components/ui"; +import { GlobalViewListItem } from "components/workspace"; // store hooks import { useGlobalView } from "hooks/store"; -// components -import { GlobalViewListItem } from "components/workspace"; -// ui -import { ViewListLoader } from "components/ui"; type Props = { searchQuery: string; @@ -29,11 +28,5 @@ export const GlobalViewsList: React.FC = observer((props) => { const filteredViewsList = getSearchedViews(searchQuery); - return ( - <> - {filteredViewsList?.map((viewId) => ( - - ))} - - ); + return <>{filteredViewsList?.map((viewId) => )}; }); diff --git a/web/components/workspace/workspace-active-cycles-upgrade.tsx b/web/components/workspace/workspace-active-cycles-upgrade.tsx index b5a61610b..23ab27acf 100644 --- a/web/components/workspace/workspace-active-cycles-upgrade.tsx +++ b/web/components/workspace/workspace-active-cycles-upgrade.tsx @@ -1,16 +1,16 @@ import React from "react"; -import Image from "next/image"; import { observer } from "mobx-react"; -// hooks -import { useUser } from "hooks/store"; -// ui -import { getButtonStyling } from "@plane/ui"; +import Image from "next/image"; // icons import { Crown } from "lucide-react"; -// helper -import { cn } from "helpers/common.helper"; +// ui +import { getButtonStyling } from "@plane/ui"; // constants import { WORKSPACE_ACTIVE_CYCLES_DETAILS } from "constants/cycle"; +// helper +import { cn } from "helpers/common.helper"; +// hooks +import { useUser } from "hooks/store"; export const WorkspaceActiveCyclesUpgrade = observer(() => { // store hooks @@ -75,7 +75,7 @@ export const WorkspaceActiveCyclesUpgrade = observer(() => {
{WORKSPACE_ACTIVE_CYCLES_DETAILS.map((item) => ( -
+

{item.title}

diff --git a/web/constants/cycle.ts b/web/constants/cycle.ts index 63900b6b7..8bb43d898 100644 --- a/web/constants/cycle.ts +++ b/web/constants/cycle.ts @@ -162,5 +162,3 @@ export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [ icon: Microscope, }, ]; - - diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index b1cfa51d7..3d99a4679 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -1,19 +1,19 @@ import { linearGradientDef } from "@nivo/core"; // assets -import UpcomingIssuesDark from "public/empty-state/dashboard/dark/upcoming-issues.svg"; -import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; -import OverdueIssuesDark from "public/empty-state/dashboard/dark/overdue-issues.svg"; -import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issues.svg"; -import CompletedIssuesDark from "public/empty-state/dashboard/dark/completed-issues.svg"; -import CompletedIssuesLight from "public/empty-state/dashboard/light/completed-issues.svg"; -// types -import { TDurationFilterOptions, TIssuesListTypes, TStateGroups } from "@plane/types"; +import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react"; +import { ContrastIcon } from "@plane/ui"; import { Props } from "components/icons/types"; +import CompletedIssuesDark from "public/empty-state/dashboard/dark/completed-issues.svg"; +import OverdueIssuesDark from "public/empty-state/dashboard/dark/overdue-issues.svg"; +import UpcomingIssuesDark from "public/empty-state/dashboard/dark/upcoming-issues.svg"; +import CompletedIssuesLight from "public/empty-state/dashboard/light/completed-issues.svg"; +import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issues.svg"; +import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg"; +// types +import { TIssuesListTypes, TStateGroups } from "@plane/types"; // constants import { EUserWorkspaceRoles } from "./workspace"; // icons -import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react"; -import { ContrastIcon } from "@plane/ui"; // gradients for issues by priority widget graph bars export const PRIORITY_GRAPH_GRADIENTS = [ @@ -116,31 +116,44 @@ export const STATE_GROUP_GRAPH_COLORS: Record = { cancelled: "#E5484D", }; +export enum EDurationFilters { + NONE = "none", + TODAY = "today", + THIS_WEEK = "this_week", + THIS_MONTH = "this_month", + THIS_YEAR = "this_year", + CUSTOM = "custom", +} + // filter duration options export const DURATION_FILTER_OPTIONS: { - key: TDurationFilterOptions; + key: EDurationFilters; label: string; }[] = [ { - key: "none", + key: EDurationFilters.NONE, label: "None", }, { - key: "today", + key: EDurationFilters.TODAY, label: "Due today", }, { - key: "this_week", - label: " Due this week", + key: EDurationFilters.THIS_WEEK, + label: "Due this week", }, { - key: "this_month", + key: EDurationFilters.THIS_MONTH, label: "Due this month", }, { - key: "this_year", + key: EDurationFilters.THIS_YEAR, label: "Due this year", }, + { + key: EDurationFilters.CUSTOM, + label: "Custom", + }, ]; // random background colors for project cards diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index a1b2b06f3..38f334b20 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -1,366 +1,516 @@ -// workspace empty state -export const WORKSPACE_EMPTY_STATE_DETAILS = { - dashboard: { +import { EUserProjectRoles } from "./project"; +import { EUserWorkspaceRoles } from "./workspace"; + +export interface EmptyStateDetails { + key: string; + title?: string; + description?: string; + path?: string; + primaryButton?: { + icon?: any; + text: string; + comicBox?: { + title?: string; + description?: string; + }; + }; + secondaryButton?: { + icon?: any; + text: string; + comicBox?: { + title?: string; + description?: string; + }; + }; + accessType?: "workspace" | "project"; + access?: EUserWorkspaceRoles | EUserProjectRoles; +} + +export type EmptyStateKeys = keyof typeof emptyStateDetails; + +export enum EmptyStateType { + WORKSPACE_DASHBOARD = "workspace-dashboard", + WORKSPACE_ANALYTICS = "workspace-analytics", + WORKSPACE_PROJECTS = "workspace-projects", + WORKSPACE_ALL_ISSUES = "workspace-all-issues", + WORKSPACE_ASSIGNED = "workspace-assigned", + WORKSPACE_CREATED = "workspace-created", + WORKSPACE_SUBSCRIBED = "workspace-subscribed", + WORKSPACE_CUSTOM_VIEW = "workspace-custom-view", + WORKSPACE_NO_PROJECTS = "workspace-no-projects", + WORKSPACE_SETTINGS_API_TOKENS = "workspace-settings-api-tokens", + WORKSPACE_SETTINGS_WEBHOOKS = "workspace-settings-webhooks", + WORKSPACE_SETTINGS_EXPORT = "workspace-settings-export", + WORKSPACE_SETTINGS_IMPORT = "workspace-settings-import", + PROFILE_ASSIGNED = "profile-assigned", + PROFILE_CREATED = "profile-created", + PROFILE_SUBSCRIBED = "profile-subscribed", + PROJECT_SETTINGS_LABELS = "project-settings-labels", + PROJECT_SETTINGS_INTEGRATIONS = "project-settings-integrations", + PROJECT_SETTINGS_ESTIMATE = "project-settings-estimate", + PROJECT_CYCLES = "project-cycles", + PROJECT_CYCLE_NO_ISSUES = "project-cycle-no-issues", + PROJECT_CYCLE_ACTIVE = "project-cycle-active", + PROJECT_CYCLE_UPCOMING = "project-cycle-upcoming", + PROJECT_CYCLE_COMPLETED = "project-cycle-completed", + PROJECT_CYCLE_DRAFT = "project-cycle-draft", + PROJECT_EMPTY_FILTER = "project-empty-filter", + PROJECT_ARCHIVED_EMPTY_FILTER = "project-archived-empty-filter", + PROJECT_DRAFT_EMPTY_FILTER = "project-draft-empty-filter", + PROJECT_NO_ISSUES = "project-no-issues", + PROJECT_ARCHIVED_NO_ISSUES = "project-archived-no-issues", + PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues", + VIEWS_EMPTY_SEARCH = "views-empty-search", + PROJECTS_EMPTY_SEARCH = "projects-empty-search", + COMMANDK_EMPTY_SEARCH = "commandK-empty-search", + MEMBERS_EMPTY_SEARCH = "members-empty-search", + PROJECT_MODULE_ISSUES = "project-module-issues", + PROJECT_MODULE = "project-module", + PROJECT_VIEW = "project-view", + PROJECT_PAGE = "project-page", + PROJECT_PAGE_ALL = "project-page-all", + PROJECT_PAGE_FAVORITE = "project-page-favorite", + PROJECT_PAGE_PRIVATE = "project-page-private", + PROJECT_PAGE_SHARED = "project-page-shared", + PROJECT_PAGE_ARCHIVED = "project-page-archived", + PROJECT_PAGE_RECENT = "project-page-recent", +} + +const emptyStateDetails = { + // workspace + "workspace-dashboard": { + key: "workspace-dashboard", title: "Overview of your projects, activity, and metrics", description: " Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.", + path: "/empty-state/onboarding/dashboard", + // path: "/empty-state/onboarding/", primaryButton: { text: "Build your first project", + comicBox: { + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }, }, - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, + + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - analytics: { + "workspace-analytics": { + key: "workspace-analytics", title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", description: "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", + path: "/empty-state/onboarding/analytics", primaryButton: { text: "Create Cycles and Modules first", + comicBox: { + title: "Analytics works best with Cycles + Modules", + description: + "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", + }, }, - comicBox: { - title: "Analytics works best with Cycles + Modules", - description: - "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", - }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - projects: { + "workspace-projects": { + key: "workspace-projects", title: "Start a Project", description: "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal.", + path: "/empty-state/onboarding/projects", primaryButton: { text: "Start your first project", + comicBox: { + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }, }, - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - "assigned-notification": { - key: "assigned-notification", - title: "No issues assigned", - description: "Updates for issues assigned to you can be seen here", - }, - "created-notification": { - key: "created-notification", - title: "No updates to issues", - description: "Updates to issues created by you can be seen here", - }, - "subscribed-notification": { - key: "subscribed-notification", - title: "No updates to issues", - description: "Updates to any issue you are subscribed to can be seen here", - }, -}; - -export const ALL_ISSUES_EMPTY_STATE_DETAILS = { - "all-issues": { - key: "all-issues", + // all-issues + "workspace-all-issues": { + key: "workspace-all-issues", title: "No issues in the project", description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", + path: "/empty-state/all-issues/all-issues", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - assigned: { - key: "assigned", + "workspace-assigned": { + key: "workspace-assigned", title: "No issues yet", description: "Issues assigned to you can be tracked from here.", + path: "/empty-state/all-issues/assigned", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - created: { - key: "created", + "workspace-created": { + key: "workspace-created", title: "No issues yet", description: "All issues created by you come here, track them here directly.", + path: "/empty-state/all-issues/created", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - subscribed: { - key: "subscribed", + "workspace-subscribed": { + key: "workspace-subscribed", title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", + path: "/empty-state/all-issues/subscribed", }, - "custom-view": { - key: "custom-view", + "workspace-custom-view": { + key: "workspace-custom-view", title: "No issues yet", description: "Issues that applies to the filters, track all of them here.", + path: "/empty-state/all-issues/custom-view", }, -}; - -export const SEARCH_EMPTY_STATE_DETAILS = { - views: { - key: "views", - title: "No matching views", - description: "No views match the search criteria. Create a new view instead.", + "workspace-no-projects": { + key: "workspace-no-projects", + title: "No project", + description: "To create issues or manage your work, you need to create a project or be a part of one.", + path: "/empty-state/onboarding/projects", + primaryButton: { + text: "Start your first project", + comicBox: { + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }, + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - projects: { - key: "projects", - title: "No matching projects", - description: "No projects detected with the matching criteria. Create a new project instead.", - }, - commandK: { - key: "commandK", - title: "No results found. ", - }, - members: { - key: "members", - title: "No matching members", - description: "Add them to the project if they are already a part of the workspace", - }, -}; - -export const WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS = { - "api-tokens": { - key: "api-tokens", + // workspace settings + "workspace-settings-api-tokens": { + key: "workspace-settings-api-tokens", title: "No API tokens created", description: "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.", + path: "/empty-state/workspace-settings/api-tokens", }, - webhooks: { - key: "webhooks", + "workspace-settings-webhooks": { + key: "workspace-settings-webhooks", title: "No webhooks added", description: "Create webhooks to receive real-time updates and automate actions.", + path: "/empty-state/workspace-settings/webhooks", }, - export: { - key: "export", + "workspace-settings-export": { + key: "workspace-settings-export", title: "No previous exports yet", description: "Anytime you export, you will also have a copy here for reference.", + path: "/empty-state/workspace-settings/exports", }, - import: { - key: "export", + "workspace-settings-import": { + key: "workspace-settings-import", title: "No previous imports yet", description: "Find all your previous imports here and download them.", + path: "/empty-state/workspace-settings/imports", }, -}; - -// profile empty state -export const PROFILE_EMPTY_STATE_DETAILS = { - assigned: { - key: "assigned", + // profile + "profile-assigned": { + key: "profile-assigned", title: "No issues are assigned to you", description: "Issues assigned to you can be tracked from here.", + path: "/empty-state/profile/assigned", }, - subscribed: { - key: "created", + "profile-created": { + key: "profile-created", title: "No issues yet", description: "All issues created by you come here, track them here directly.", + path: "/empty-state/profile/created", }, - created: { - key: "subscribed", + "profile-subscribed": { + key: "profile-subscribed", title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", + path: "/empty-state/profile/subscribed", }, -}; - -// project empty state - -export const PROJECT_SETTINGS_EMPTY_STATE_DETAILS = { - labels: { - key: "labels", + // project settings + "project-settings-labels": { + key: "project-settings-labels", title: "No labels yet", description: "Create labels to help organize and filter issues in you project.", + path: "/empty-state/project-settings/labels", }, - integrations: { - key: "integrations", + "project-settings-integrations": { + key: "project-settings-integrations", title: "No integrations configured", description: "Configure GitHub and other integrations to sync your project issues.", + path: "/empty-state/project-settings/integrations", }, - estimate: { - key: "estimate", + "project-settings-estimate": { + key: "project-settings-estimate", title: "No estimates added", description: "Create a set of estimates to communicate the amount of work per issue.", + path: "/empty-state/project-settings/estimates", }, -}; - -export const CYCLE_EMPTY_STATE_DETAILS = { - cycles: { + // project cycles + "project-cycles": { + key: "project-cycles", title: "Group and timebox your work in Cycles.", description: "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", - comicBox: { - title: "Cycles are repetitive time-boxes.", - description: - "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", - }, + path: "/empty-state/onboarding/cycles", primaryButton: { text: "Set your first cycle", + comicBox: { + title: "Cycles are repetitive time-boxes.", + description: + "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", + }, }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - "no-issues": { - key: "no-issues", + "project-cycle-no-issues": { + key: "project-cycle-no-issues", title: "No issues added to the cycle", description: "Add or create issues you wish to timebox and deliver within this cycle", + path: "/empty-state/cycle-issues/", primaryButton: { text: "Create new issue ", }, secondaryButton: { text: "Add an existing issue", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - active: { - key: "active", + "project-cycle-active": { + key: "project-cycle-active", title: "No active cycles", description: "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", + path: "/empty-state/cycle/active", }, - upcoming: { - key: "upcoming", + "project-cycle-upcoming": { + key: "project-cycle-upcoming", title: "No upcoming cycles", description: "Upcoming cycles on deck! Just add dates to cycles in draft, and they'll show up right here.", + path: "/empty-state/cycle/upcoming", }, - completed: { - key: "completed", + "project-cycle-completed": { + key: "project-cycle-completed", title: "No completed cycles", description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.", + path: "/empty-state/cycle/completed", }, - draft: { - key: "draft", + "project-cycle-draft": { + key: "project-cycle-draft", title: "No draft cycles", description: "No dates added in cycles? Find them here as drafts.", + path: "/empty-state/cycle/draft", }, -}; - -export const EMPTY_FILTER_STATE_DETAILS = { - archived: { - key: "archived", + // empty filters + "project-empty-filter": { + key: "project-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - draft: { - key: "draft", + "project-archived-empty-filter": { + key: "project-archived-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - project: { - key: "project", + "project-draft-empty-filter": { + key: "project-draft-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const EMPTY_ISSUE_STATE_DETAILS = { - archived: { - key: "archived", - title: "No archived issues yet", - description: - "Archived issues help you remove issues you completed or canceled from focus. You can set automation to auto archive issues and find them here.", - primaryButton: { - text: "Set automation", - }, - }, - draft: { - key: "draft", - title: "No draft issues yet", - description: - "Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.", - }, - project: { - key: "project", + // project issues + "project-no-issues": { + key: "project-no-issues", title: "Create an issue and assign it to someone, even yourself", description: "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", - comicBox: { - title: "Issues are building blocks in Plane.", - description: - "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", - }, + path: "/empty-state/onboarding/issues", primaryButton: { text: "Create your first issue", + comicBox: { + title: "Issues are building blocks in Plane.", + description: + "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const MODULE_EMPTY_STATE_DETAILS = { - "no-issues": { - key: "no-issues", + "project-archived-no-issues": { + key: "project-archived-no-issues", + title: "No archived issues yet", + description: + "Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.", + path: "/empty-state/archived/empty-issues", + primaryButton: { + text: "Set automation", + }, + accessType: "project", + access: EUserProjectRoles.MEMBER, + }, + "project-draft-no-issues": { + key: "project-draft-no-issues", + title: "No draft issues yet", + description: + "Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.", + path: "/empty-state/draft/draft-issues-empty", + }, + "views-empty-search": { + key: "views-empty-search", + title: "No matching views", + description: "No views match the search criteria. Create a new view instead.", + path: "/empty-state/search/search", + }, + "projects-empty-search": { + key: "projects-empty-search", + title: "No matching projects", + description: "No projects detected with the matching criteria. Create a new project instead.", + path: "/empty-state/search/project", + }, + "commandK-empty-search": { + key: "commandK-empty-search", + title: "No results found. ", + path: "/empty-state/search/search", + }, + "members-empty-search": { + key: "members-empty-search", + title: "No matching members", + description: "Add them to the project if they are already a part of the workspace", + path: "/empty-state/search/member", + }, + // project module + "project-module-issues": { + key: "project-modules-issues", title: "No issues in the module", description: "Create or add issues which you want to accomplish as part of this module", + path: "/empty-state/module-issues/", primaryButton: { text: "Create new issue ", }, secondaryButton: { text: "Add an existing issue", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - modules: { + "project-module": { + key: "project-module", title: "Map your project milestones to Modules and track aggregated work easily.", description: "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.", - - comicBox: { - title: "Modules help group work by hierarchy.", - description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", - }, + path: "/empty-state/onboarding/modules", primaryButton: { text: "Build your first module", + comicBox: { + title: "Modules help group work by hierarchy.", + description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const VIEW_EMPTY_STATE_DETAILS = { - "project-views": { + // project views + "project-view": { + key: "project-view", title: "Save filtered views for your project. Create as many as you need", description: "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.", - comicBox: { - title: "Views work atop Issue properties.", - description: "You can create a view from here with as many properties as filters as you see fit.", - }, + path: "/empty-state/onboarding/views", primaryButton: { text: "Create your first view", + comicBox: { + title: "Views work atop Issue properties.", + description: "You can create a view from here with as many properties as filters as you see fit.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const PAGE_EMPTY_STATE_DETAILS = { - pages: { + // project pages + "project-page": { key: "pages", title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started", description: "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", + path: "/empty-state/onboarding/pages", primaryButton: { text: "Create your first page", + comicBox: { + title: "A page can be a doc or a doc of docs.", + description: + "We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.", + }, }, - comicBox: { - title: "A page can be a doc or a doc of docs.", - description: - "We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.", - }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - All: { - key: "all", + "project-page-all": { + key: "project-page-all", title: "Write a note, a doc, or a full knowledge base", description: "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!", + path: "/empty-state/pages/all", }, - Favorites: { - key: "favorites", + "project-page-favorite": { + key: "project-page-favorite", title: "No favorite pages yet", description: "Favorites for quick access? mark them and find them right here.", + path: "/empty-state/pages/favorites", }, - Private: { - key: "private", + "project-page-private": { + key: "project-page-private", title: "No private pages yet", description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", + path: "/empty-state/pages/private", }, - Shared: { - key: "shared", + "project-page-shared": { + key: "project-page-shared", title: "No shared pages yet", description: "See pages shared with everyone in your project right here.", + path: "/empty-state/pages/shared", }, - Archived: { - key: "archived", + "project-page-archived": { + key: "project-page-archived", title: "No archived pages yet", description: "Archive pages not on your radar. Access them here when needed.", + path: "/empty-state/pages/archived", }, - Recent: { - key: "recent", + "project-page-recent": { + key: "project-page-recent", title: "Write a note, a doc, or a full knowledge base", description: "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated", + path: "/empty-state/pages/recent", primaryButton: { text: "Create new page", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; +} as const; + +export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/constants/event-tracker.ts b/web/constants/event-tracker.ts index 37a18a37d..7edfccba5 100644 --- a/web/constants/event-tracker.ts +++ b/web/constants/event-tracker.ts @@ -112,16 +112,16 @@ export const getIssueEventPayload = (props: IssueEventProps) => { updated_from: props.path?.includes("workspace-views") ? "All views" : props.path?.includes("cycles") - ? "Cycle" - : props.path?.includes("modules") - ? "Module" - : props.path?.includes("views") - ? "Project view" - : props.path?.includes("inbox") - ? "Inbox" - : props.path?.includes("draft") - ? "Draft" - : "Project", + ? "Cycle" + : props.path?.includes("modules") + ? "Module" + : props.path?.includes("views") + ? "Project view" + : props.path?.includes("inbox") + ? "Inbox" + : props.path?.includes("draft") + ? "Draft" + : "Project", }; } return eventPayload; diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index 86386e968..3b2e97c38 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -164,7 +164,7 @@ export const USER_ISSUES = (workspaceSlug: string, params: any) => { return `USER_ISSUES_${workspaceSlug.toUpperCase()}_${paramsKey}`; }; -export const USER_ACTIVITY = "USER_ACTIVITY"; +export const USER_ACTIVITY = (params: { cursor?: string }) => `USER_ACTIVITY_${params?.cursor}`; export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) => `USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`; export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId.toUpperCase()}`; @@ -284,8 +284,13 @@ export const getPaginatedNotificationKey = (index: number, prevData: any, worksp // profile export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) => `USER_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; -export const USER_PROFILE_ACTIVITY = (workspaceSlug: string, userId: string) => - `USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; +export const USER_PROFILE_ACTIVITY = ( + workspaceSlug: string, + userId: string, + params: { + cursor?: string; + } +) => `USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${params?.cursor}`; export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: string) => `USER_PROFILE_PROJECT_SEGREGATION_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; export const USER_PROFILE_ISSUES = (workspaceSlug: string, userId: string, params: any) => { diff --git a/web/constants/issue.ts b/web/constants/issue.ts index b2a8cd855..7e0fd7a06 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -49,22 +49,6 @@ export const ISSUE_PRIORITIES: { { key: "none", title: "None" }, ]; -export const ISSUE_START_DATE_OPTIONS = [ - { key: "last_week", title: "Last Week" }, - { key: "2_weeks_from_now", title: "2 weeks from now" }, - { key: "1_month_from_now", title: "1 month from now" }, - { key: "2_months_from_now", title: "2 months from now" }, - { key: "custom", title: "Custom" }, -]; - -export const ISSUE_DUE_DATE_OPTIONS = [ - { key: "last_week", title: "Last Week" }, - { key: "2_weeks_from_now", title: "2 weeks from now" }, - { key: "1_month_from_now", title: "1 month from now" }, - { key: "2_months_from_now", title: "2 months from now" }, - { key: "custom", title: "Custom" }, -]; - export const ISSUE_GROUP_BY_OPTIONS: { key: TIssueGroupByOptions; title: string; @@ -73,6 +57,8 @@ export const ISSUE_GROUP_BY_OPTIONS: { { key: "state_detail.group", title: "State Groups" }, { key: "priority", title: "Priority" }, { key: "project", title: "Project" }, // required this on my issues + { key: "cycle", title: "Cycle" }, // required this on my issues + { key: "module", title: "Module" }, // required this on my issues { key: "labels", title: "Labels" }, { key: "assignees", title: "Assignees" }, { key: "created_by", title: "Created By" }, @@ -140,81 +126,6 @@ export const ISSUE_LAYOUTS: { { key: "gantt_chart", title: "Gantt Chart Layout", icon: GanttChartSquare }, ]; -export const ISSUE_LIST_FILTERS = [ - { key: "mentions", title: "Mentions" }, - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_KANBAN_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_CALENDER_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, -]; - -export const ISSUE_SPREADSHEET_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_GANTT_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_LIST_DISPLAY_FILTERS = [ - { key: "group_by", title: "Group By" }, - { key: "order_by", title: "Order By" }, - { key: "issue_type", title: "Issue Type" }, - { key: "sub_issue", title: "Sub Issue" }, - { key: "show_empty_groups", title: "Show Empty Groups" }, -]; - -export const ISSUE_KANBAN_DISPLAY_FILTERS = [ - { key: "group_by", title: "Group By" }, - { key: "order_by", title: "Order By" }, - { key: "issue_type", title: "Issue Type" }, - { key: "sub_issue", title: "Sub Issue" }, - { key: "show_empty_groups", title: "Show Empty Groups" }, -]; - -export const ISSUE_CALENDER_DISPLAY_FILTERS = [{ key: "issue_type", title: "Issue Type" }]; - -export const ISSUE_SPREADSHEET_DISPLAY_FILTERS = [{ key: "issue_type", title: "Issue Type" }]; - -export const ISSUE_GANTT_DISPLAY_FILTERS = [ - { key: "order_by", title: "Order By" }, - { key: "issue_type", title: "Issue Type" }, - { key: "sub_issue", title: "Sub Issue" }, -]; - export interface ILayoutDisplayFiltersOptions { filters: (keyof IIssueFilterOptions)[]; display_properties: boolean; @@ -263,10 +174,30 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, archived_issues: { list: { - filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: true, display_filters: { - group_by: ["state", "state_detail.group", "priority", "labels", "assignees", "created_by", null], + group_by: [ + "state", + "cycle", + "module", + "state_detail.group", + "priority", + "labels", + "assignees", + "created_by", + null, + ], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -278,10 +209,10 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, draft_issues: { list: { - filters: ["priority", "state_group", "labels", "start_date", "target_date"], + filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"], display_properties: true, display_filters: { - group_by: ["state_detail.group", "priority", "project", "labels", null], + group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -291,10 +222,10 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, kanban: { - filters: ["priority", "state_group", "labels", "start_date", "target_date"], + filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"], display_properties: true, display_filters: { - group_by: ["state_detail.group", "priority", "project", "labels"], + group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels"], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -350,10 +281,21 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, issues: { list: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: true, display_filters: { - group_by: ["state", "priority", "labels", "assignees", "created_by", null], + group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -363,11 +305,22 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, kanban: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: true, display_filters: { - group_by: ["state", "priority", "labels", "assignees", "created_by"], - sub_group_by: ["state", "priority", "labels", "assignees", "created_by", null], + group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by"], + sub_group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"], type: [null, "active", "backlog"], }, @@ -377,7 +330,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, calendar: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date"], + filters: ["priority", "state", "cycle", "module", "assignees", "mentions", "created_by", "labels", "start_date"], display_properties: true, display_filters: { type: [null, "active", "backlog"], @@ -388,7 +341,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, spreadsheet: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: true, display_filters: { order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], @@ -400,7 +364,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, gantt_chart: { - filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + filters: [ + "priority", + "state", + "cycle", + "module", + "assignees", + "mentions", + "created_by", + "labels", + "start_date", + "target_date", + ], display_properties: false, display_filters: { order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], diff --git a/web/constants/profile.ts b/web/constants/profile.ts index 463fd27ee..4d8e37640 100644 --- a/web/constants/profile.ts +++ b/web/constants/profile.ts @@ -63,4 +63,9 @@ export const PROFILE_ADMINS_TAB = [ label: "Subscribed", selected: "/[workspaceSlug]/profile/[userId]/subscribed", }, + { + route: "activity", + label: "Activity", + selected: "/[workspaceSlug]/profile/[userId]/activity", + }, ]; diff --git a/web/constants/spreadsheet.ts b/web/constants/spreadsheet.ts index aa588d9e1..50d6c15df 100644 --- a/web/constants/spreadsheet.ts +++ b/web/constants/spreadsheet.ts @@ -1,8 +1,7 @@ -import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; -import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui"; -import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock } from "lucide-react"; import { FC } from "react"; import { ISvgIcons } from "@plane/ui/src/icons/type"; +import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock } from "lucide-react"; +import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui"; import { SpreadsheetAssigneeColumn, SpreadsheetAttachmentColumn, @@ -19,6 +18,7 @@ import { SpreadsheetSubIssueColumn, SpreadsheetUpdatedOnColumn, } from "components/issues/issue-layouts/spreadsheet"; +import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; export const SPREADSHEET_PROPERTY_DETAILS: { [key: string]: { diff --git a/web/constants/workspace.ts b/web/constants/workspace.ts index 1471de395..7ae89d5d6 100644 --- a/web/constants/workspace.ts +++ b/web/constants/workspace.ts @@ -1,14 +1,14 @@ // services images -import GithubLogo from "public/services/github.png"; -import JiraLogo from "public/services/jira.svg"; +import { SettingIcon } from "components/icons"; +import { Props } from "components/icons/types"; import CSVLogo from "public/services/csv.svg"; import ExcelLogo from "public/services/excel.svg"; +import GithubLogo from "public/services/github.png"; +import JiraLogo from "public/services/jira.svg"; import JSONLogo from "public/services/json.svg"; // types import { TStaticViewTypes } from "@plane/types"; -import { Props } from "components/icons/types"; // icons -import { SettingIcon } from "components/icons"; export enum EUserWorkspaceRoles { GUEST = 5, diff --git a/web/contexts/toast.context.tsx b/web/contexts/toast.context.tsx deleted file mode 100644 index 30e100b20..000000000 --- a/web/contexts/toast.context.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { createContext, useCallback, useReducer } from "react"; -// uuid -import { v4 as uuid } from "uuid"; -// components -import ToastAlert from "components/toast-alert"; - -export const toastContext = createContext({} as ContextType); - -// types -type ToastAlert = { - id: string; - title: string; - message?: string; - type: "success" | "error" | "warning" | "info"; -}; - -type ReducerActionType = { - type: "SET_TOAST_ALERT" | "REMOVE_TOAST_ALERT"; - payload: ToastAlert; -}; - -type ContextType = { - alerts?: ToastAlert[]; - removeAlert: (id: string) => void; - setToastAlert: (data: { - title: string; - type?: "success" | "error" | "warning" | "info" | undefined; - message?: string | undefined; - }) => void; -}; - -type StateType = { - toastAlerts?: ToastAlert[]; -}; - -type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType; - -export const initialState: StateType = { - toastAlerts: [], -}; - -export const reducer: ReducerFunctionType = (state, action) => { - const { type, payload } = action; - - switch (type) { - case "SET_TOAST_ALERT": - return { - ...state, - toastAlerts: [...(state.toastAlerts ?? []), payload], - }; - - case "REMOVE_TOAST_ALERT": - return { - ...state, - toastAlerts: state.toastAlerts?.filter((toastAlert) => toastAlert.id !== payload.id), - }; - - default: { - return state; - } - } -}; - -export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [state, dispatch] = useReducer(reducer, initialState); - - const removeAlert = useCallback((id: string) => { - dispatch({ - type: "REMOVE_TOAST_ALERT", - payload: { id, title: "", message: "", type: "success" }, - }); - }, []); - - const setToastAlert = useCallback( - (data: { title: string; type?: "success" | "error" | "warning" | "info"; message?: string }) => { - const id = uuid(); - const { title, type, message } = data; - dispatch({ - type: "SET_TOAST_ALERT", - payload: { id, title, message, type: type ?? "success" }, - }); - - const timer = setTimeout(() => { - removeAlert(id); - clearTimeout(timer); - }, 3000); - }, - [removeAlert] - ); - - return ( - - - {children} - - ); -}; diff --git a/web/contexts/user-notification-context.tsx b/web/contexts/user-notification-context.tsx index b55a05771..ef3af2124 100644 --- a/web/contexts/user-notification-context.tsx +++ b/web/contexts/user-notification-context.tsx @@ -3,9 +3,9 @@ import { createContext, useCallback, useEffect, useReducer } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services +import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys"; import { NotificationService } from "services/notification.service"; // fetch-keys -import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys"; // type import type { NotificationType, NotificationCount, IUserNotification } from "@plane/types"; diff --git a/web/helpers/analytics.helper.ts b/web/helpers/analytics.helper.ts index 58a456ed7..dfa98d7ea 100644 --- a/web/helpers/analytics.helper.ts +++ b/web/helpers/analytics.helper.ts @@ -1,13 +1,13 @@ // nivo import { BarDatum } from "@nivo/bar"; // helpers +import { DATE_KEYS } from "constants/analytics"; +import { MONTHS_LIST } from "constants/calendar"; +import { STATE_GROUPS } from "constants/state"; import { addSpaceIfCamelCase, capitalizeFirstLetter, generateRandomColor } from "helpers/string.helper"; // types import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse, TStateGroups } from "@plane/types"; // constants -import { STATE_GROUPS } from "constants/state"; -import { MONTHS_LIST } from "constants/calendar"; -import { DATE_KEYS } from "constants/analytics"; export const convertResponseToBarGraphData = ( response: IAnalyticsData | undefined, @@ -36,8 +36,8 @@ export const convertResponseToBarGraphData = ( name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(key) : params.x_axis === "priority" || params.x_axis === "state__group" - ? capitalizeFirstLetter(key) - : key, + ? capitalizeFirstLetter(key) + : key, ...segments, }); } else { @@ -49,8 +49,8 @@ export const convertResponseToBarGraphData = ( name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(item.dimension) : params.x_axis === "priority" || params.x_axis === "state__group" - ? capitalizeFirstLetter(item.dimension ?? "None") - : item.dimension ?? "None", + ? capitalizeFirstLetter(item.dimension ?? "None") + : item.dimension ?? "None", [yAxisKey]: item[yAxisKey] ?? 0, }); } @@ -84,12 +84,12 @@ export const generateBarColor = ( priority === "urgent" ? "#ef4444" : priority === "high" - ? "#f97316" - : priority === "medium" - ? "#eab308" - : priority === "low" - ? "#22c55e" - : "#ced4da"; + ? "#f97316" + : priority === "medium" + ? "#eab308" + : priority === "low" + ? "#22c55e" + : "#ced4da"; } return color ?? generateRandomColor(value); diff --git a/web/helpers/calendar.helper.ts b/web/helpers/calendar.helper.ts index e570a5c9a..6c648dd6b 100644 --- a/web/helpers/calendar.helper.ts +++ b/web/helpers/calendar.helper.ts @@ -1,7 +1,7 @@ // helpers +import { ICalendarDate, ICalendarPayload } from "components/issues"; import { getWeekNumberOfDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { ICalendarDate, ICalendarPayload } from "components/issues"; export const formatDate = (date: Date, format: string): string => { const day = date.getDate(); diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index 90319a90b..0c3b8da07 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -1,36 +1,40 @@ import { endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYear } from "date-fns"; // helpers -import { renderFormattedPayloadDate } from "./date-time.helper"; +import { renderFormattedDate, renderFormattedPayloadDate } from "./date-time.helper"; // types -import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; +import { TIssuesListTypes } from "@plane/types"; +// constants +import { DURATION_FILTER_OPTIONS, EDurationFilters } from "constants/dashboard"; /** * @description returns date range based on the duration filter * @param duration */ -export const getCustomDates = (duration: TDurationFilterOptions): string => { +export const getCustomDates = (duration: EDurationFilters, customDates: string[]): string => { const today = new Date(); let firstDay, lastDay; switch (duration) { - case "none": + case EDurationFilters.NONE: return ""; - case "today": + case EDurationFilters.TODAY: firstDay = renderFormattedPayloadDate(today); lastDay = renderFormattedPayloadDate(today); return `${firstDay};after,${lastDay};before`; - case "this_week": + case EDurationFilters.THIS_WEEK: firstDay = renderFormattedPayloadDate(startOfWeek(today)); lastDay = renderFormattedPayloadDate(endOfWeek(today)); return `${firstDay};after,${lastDay};before`; - case "this_month": + case EDurationFilters.THIS_MONTH: firstDay = renderFormattedPayloadDate(startOfMonth(today)); lastDay = renderFormattedPayloadDate(endOfMonth(today)); return `${firstDay};after,${lastDay};before`; - case "this_year": + case EDurationFilters.THIS_YEAR: firstDay = renderFormattedPayloadDate(startOfYear(today)); lastDay = renderFormattedPayloadDate(endOfYear(today)); return `${firstDay};after,${lastDay};before`; + case EDurationFilters.CUSTOM: + return customDates.join(","); } }; @@ -45,10 +49,10 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => { type === "pending" ? "?state_group=backlog,unstarted,started" : type === "upcoming" - ? `?target_date=${today};after` - : type === "overdue" - ? `?target_date=${today};before` - : "?state_group=completed"; + ? `?target_date=${today};after` + : type === "overdue" + ? `?target_date=${today};before` + : "?state_group=completed"; return filterParams; }; @@ -58,7 +62,7 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => { * @param duration * @param tab */ -export const getTabKey = (duration: TDurationFilterOptions, tab: TIssuesListTypes | undefined): TIssuesListTypes => { +export const getTabKey = (duration: EDurationFilters, tab: TIssuesListTypes | undefined): TIssuesListTypes => { if (!tab) return "completed"; if (tab === "completed") return tab; @@ -69,3 +73,21 @@ export const getTabKey = (duration: TDurationFilterOptions, tab: TIssuesListType else return "upcoming"; } }; + +/** + * @description returns the label for the duration filter dropdown + * @param duration + * @param customDates + */ +export const getDurationFilterDropdownLabel = (duration: EDurationFilters, customDates: string[]): string => { + if (duration !== "custom") return DURATION_FILTER_OPTIONS.find((option) => option.key === duration)?.label ?? ""; + else { + const afterDate = customDates.find((date) => date.includes("after"))?.split(";")[0]; + const beforeDate = customDates.find((date) => date.includes("before"))?.split(";")[0]; + + if (afterDate && beforeDate) return `${renderFormattedDate(afterDate)} - ${renderFormattedDate(beforeDate)}`; + else if (afterDate) return `After ${renderFormattedDate(afterDate)}`; + else if (beforeDate) return `Before ${renderFormattedDate(beforeDate)}`; + else return ""; + } +}; diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 026211634..513f9b6c4 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -30,7 +30,7 @@ export const renderEmoji = ( if (typeof emoji === "object") return ( - + {emoji.name} ); @@ -54,3 +54,12 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: return groupedReactions; }; + +export const convertHexEmojiToDecimal = (emojiUnified: string): string => { + if (!emojiUnified) return ""; + + return emojiUnified + .split("-") + .map((e) => parseInt(e, 16)) + .join("-"); +}; diff --git a/web/helpers/filter.helper.ts b/web/helpers/filter.helper.ts index 0b30f95e1..d31a25b3d 100644 --- a/web/helpers/filter.helper.ts +++ b/web/helpers/filter.helper.ts @@ -13,4 +13,3 @@ export const calculateTotalFilters = (filters: IIssueFilterOptions): number => ) .reduce((curr, prev) => curr + prev, 0) : 0; - diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 831cb321e..3e6689151 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -1,8 +1,12 @@ -import { v4 as uuidv4 } from "uuid"; import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; +import { v4 as uuidv4 } from "uuid"; // helpers -import { orderArrayBy } from "helpers/array.helper"; // types +import { IGanttBlock } from "components/gantt-chart"; +// constants +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; +import { orderArrayBy } from "helpers/array.helper"; import { TIssue, TIssueGroupByOptions, @@ -11,10 +15,6 @@ import { TIssueParams, TStateGroups, } from "@plane/types"; -import { IGanttBlock } from "components/gantt-chart"; -// constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { STATE_GROUPS } from "constants/state"; type THandleIssuesMutation = ( formData: Partial, @@ -171,3 +171,16 @@ export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => start_date: block.start_date ? new Date(block.start_date) : null, target_date: block.target_date ? new Date(block.target_date) : null, })); + +export function getChangedIssuefields(formData: Partial, dirtyFields: { [key: string]: boolean | undefined }) { + const changedFields: Partial = {}; + + const dirtyFieldKeys = Object.keys(dirtyFields) as (keyof TIssue)[]; + for (const dirtyField of dirtyFieldKeys) { + if (!!dirtyFields[dirtyField]) { + changedFields[dirtyField] = formData[dirtyField]; + } + } + + return changedFields; +} diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index 8d78964ee..441c14a42 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -43,3 +43,6 @@ export const orderJoinedProjects = ( return updatedSortOrder; }; + +export const projectIdentifierSanitizer = (identifier: string): string => + identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index d30b29b52..ad87c2e75 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -1,10 +1,10 @@ +import * as DOMPurify from "dompurify"; import { CYCLE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, VIEW_ISSUES, } from "constants/fetch-keys"; -import * as DOMPurify from 'dompurify'; export const addSpaceIfCamelCase = (str: string) => { if (str === undefined || str === null) return ""; @@ -172,10 +172,10 @@ export const getFetchKeysForIssueMutation = (options: { const ganttFetchKey = cycleId ? { ganttFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), ganttParams) } : moduleId - ? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) } - : viewId - ? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) } - : { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) }; + ? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) } + : viewId + ? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) } + : { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) }; return { ...ganttFetchKey, diff --git a/web/helpers/theme.helper.ts b/web/helpers/theme.helper.ts index 16cd8cd79..a9aa5b913 100644 --- a/web/helpers/theme.helper.ts +++ b/web/helpers/theme.helper.ts @@ -118,3 +118,6 @@ export const unsetCustomCssVariables = () => { dom?.style.removeProperty("--color-scheme"); } }; + +export const resolveGeneralTheme = (resolvedTheme: string | undefined) => + resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system"; diff --git a/web/hooks/store/index.ts b/web/hooks/store/index.ts index 2349b1585..ff036a529 100644 --- a/web/hooks/store/index.ts +++ b/web/hooks/store/index.ts @@ -1,5 +1,5 @@ export * from "./use-application"; -export * from "./use-event-tracker" +export * from "./use-event-tracker"; export * from "./use-calendar-view"; export * from "./use-cycle"; export * from "./use-dashboard"; diff --git a/web/hooks/store/use-inbox-issues.ts b/web/hooks/store/use-inbox-issues.ts index 2b2941f84..1196eae90 100644 --- a/web/hooks/store/use-inbox-issues.ts +++ b/web/hooks/store/use-inbox-issues.ts @@ -2,8 +2,8 @@ import { useContext } from "react"; // mobx store import { StoreContext } from "contexts/store-context"; // types -import { IInboxIssue } from "store/inbox/inbox_issue.store"; import { IInboxFilter } from "store/inbox/inbox_filter.store"; +import { IInboxIssue } from "store/inbox/inbox_issue.store"; export const useInboxIssues = (): { issues: IInboxIssue; diff --git a/web/hooks/store/use-issues.ts b/web/hooks/store/use-issues.ts index f2da9d954..ed270c9ec 100644 --- a/web/hooks/store/use-issues.ts +++ b/web/hooks/store/use-issues.ts @@ -1,19 +1,19 @@ import { useContext } from "react"; import merge from "lodash/merge"; // mobx store +import { EIssuesStoreType } from "constants/issue"; import { StoreContext } from "contexts/store-context"; // types -import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "store/issue/workspace"; +import { IArchivedIssues, IArchivedIssuesFilter } from "store/issue/archived"; +import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; +import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; +import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; -import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; -import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; -import { IArchivedIssues, IArchivedIssuesFilter } from "store/issue/archived"; -import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft"; +import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "store/issue/workspace"; import { TIssueMap } from "@plane/types"; // constants -import { EIssuesStoreType } from "constants/issue"; type defaultIssueStore = { issueMap: TIssueMap; diff --git a/web/hooks/use-comment-reaction.tsx b/web/hooks/use-comment-reaction.tsx index 2327fddcd..3750160b0 100644 --- a/web/hooks/use-comment-reaction.tsx +++ b/web/hooks/use-comment-reaction.tsx @@ -2,9 +2,9 @@ import useSWR from "swr"; // fetch keys import { COMMENT_REACTION_LIST } from "constants/fetch-keys"; // services +import { groupReactions } from "helpers/emoji.helper"; import { IssueReactionService } from "services/issue"; // helpers -import { groupReactions } from "helpers/emoji.helper"; import { useUser } from "./store"; // hooks diff --git a/web/hooks/use-draggable-portal.ts b/web/hooks/use-draggable-portal.ts index 383c277f3..325f8b268 100644 --- a/web/hooks/use-draggable-portal.ts +++ b/web/hooks/use-draggable-portal.ts @@ -1,6 +1,6 @@ -import { createPortal } from "react-dom"; import { useEffect, useRef } from "react"; import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { createPortal } from "react-dom"; const useDraggableInPortal = () => { const self = useRef(); diff --git a/web/hooks/use-dropdown-key-down.tsx b/web/hooks/use-dropdown-key-down.tsx index 228e35575..174cfdd8a 100644 --- a/web/hooks/use-dropdown-key-down.tsx +++ b/web/hooks/use-dropdown-key-down.tsx @@ -1,9 +1,11 @@ import { useCallback } from "react"; type TUseDropdownKeyDown = { - (onEnterKeyDown: () => void, onEscKeyDown: () => void, stopPropagation?: boolean): ( - event: React.KeyboardEvent - ) => void; + ( + onEnterKeyDown: () => void, + onEscKeyDown: () => void, + stopPropagation?: boolean + ): (event: React.KeyboardEvent) => void; }; export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown, stopPropagation = true) => { diff --git a/web/hooks/use-issues-actions.tsx b/web/hooks/use-issues-actions.tsx new file mode 100644 index 000000000..94add6285 --- /dev/null +++ b/web/hooks/use-issues-actions.tsx @@ -0,0 +1,576 @@ +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { useApplication, useIssues } from "./store"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TIssue, + TIssueKanbanFilters, + TLoader, +} from "@plane/types"; +import { useCallback, useMemo } from "react"; + +interface IssueActions { + fetchIssues?: (projectId: string, loadType: TLoader) => Promise; + removeIssue: (projectId: string, issueId: string) => Promise; + createIssue?: (projectId: string, data: Partial) => Promise; + updateIssue?: (projectId: string, issueId: string, data: Partial) => Promise; + removeIssueFromView?: (projectId: string, issueId: string) => Promise; + archiveIssue?: (projectId: string, issueId: string) => Promise; + restoreIssue?: (projectId: string, issueId: string) => Promise; + updateFilters: ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => Promise; +} + +export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => { + const projectIssueActions = useProjectIssueActions(); + const cycleIssueActions = useCycleIssueActions(); + const moduleIssueActions = useModuleIssueActions(); + const profileIssueActions = useProfileIssueActions(); + const projectViewIssueActions = useProjectViewIssueActions(); + const draftIssueActions = useDraftIssueActions(); + const archivedIssueActions = useArchivedIssueActions(); + const globalIssueActions = useGlobalIssueActions(); + + switch (storeType) { + case EIssuesStoreType.PROJECT_VIEW: + return projectViewIssueActions; + case EIssuesStoreType.PROFILE: + return profileIssueActions; + case EIssuesStoreType.CYCLE: + return cycleIssueActions; + case EIssuesStoreType.MODULE: + return moduleIssueActions; + case EIssuesStoreType.ARCHIVED: + return archivedIssueActions; + case EIssuesStoreType.DRAFT: + return draftIssueActions; + case EIssuesStoreType.GLOBAL: + return globalIssueActions; + case EIssuesStoreType.PROJECT: + default: + return projectIssueActions; + } +}; + +const useProjectIssueActions = () => { + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); + + const { + router: { workspaceSlug }, + } = useApplication(); + + const fetchIssues = useCallback( + async (projectId: string, loadType: TLoader) => { + if (!workspaceSlug) return; + return await issues.fetchIssues(workspaceSlug, projectId, loadType); + }, + [issues.fetchIssues, workspaceSlug] + ); + const createIssue = useCallback( + async (projectId: string, data: Partial) => { + if (!workspaceSlug) return; + return await issues.createIssue(workspaceSlug, projectId, data); + }, + [issues.createIssue, workspaceSlug] + ); + const updateIssue = useCallback( + async (projectId: string, issueId: string, data: Partial) => { + if (!workspaceSlug) return; + return await issues.updateIssue(workspaceSlug, projectId, issueId, data); + }, + [issues.updateIssue, workspaceSlug] + ); + const removeIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!workspaceSlug) return; + return await issues.removeIssue(workspaceSlug, projectId, issueId); + }, + [issues.removeIssue, workspaceSlug] + ); + const archiveIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!workspaceSlug) return; + return await issues.archiveIssue(workspaceSlug, projectId, issueId); + }, + [issues.archiveIssue, workspaceSlug] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + if (!workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters); + }, + [issuesFilter.updateFilters, workspaceSlug] + ); + + return useMemo( + () => ({ + fetchIssues, + createIssue, + updateIssue, + removeIssue, + archiveIssue, + updateFilters, + }), + [fetchIssues, createIssue, updateIssue, removeIssue, archiveIssue, updateFilters] + ); +}; + +const useCycleIssueActions = () => { + const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + + const { + router: { workspaceSlug, cycleId }, + } = useApplication(); + + const fetchIssues = useCallback( + async (projectId: string, loadType: TLoader) => { + if (!cycleId || !workspaceSlug) return; + return await issues.fetchIssues(workspaceSlug, projectId, loadType, cycleId); + }, + [issues.fetchIssues, cycleId, workspaceSlug] + ); + const createIssue = useCallback( + async (projectId: string, data: Partial) => { + if (!cycleId || !workspaceSlug) return; + return await issues.createIssue(workspaceSlug, projectId, data, cycleId); + }, + [issues.createIssue, cycleId, workspaceSlug] + ); + const updateIssue = useCallback( + async (projectId: string, issueId: string, data: Partial) => { + if (!cycleId || !workspaceSlug) return; + return await issues.updateIssue(workspaceSlug, projectId, issueId, data, cycleId); + }, + [issues.updateIssue, cycleId, workspaceSlug] + ); + const removeIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!cycleId || !workspaceSlug) return; + return await issues.removeIssue(workspaceSlug, projectId, issueId, cycleId); + }, + [issues.removeIssue, cycleId, workspaceSlug] + ); + const removeIssueFromView = useCallback( + async (projectId: string, issueId: string) => { + if (!cycleId || !workspaceSlug) return; + return await issues.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); + }, + [issues.removeIssueFromCycle, cycleId, workspaceSlug] + ); + const archiveIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!cycleId || !workspaceSlug) return; + return await issues.archiveIssue(workspaceSlug, projectId, issueId, cycleId); + }, + [issues.archiveIssue, cycleId, workspaceSlug] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + if (!cycleId || !workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, cycleId); + }, + [issuesFilter.updateFilters, cycleId, workspaceSlug] + ); + + return useMemo( + () => ({ + fetchIssues, + createIssue, + updateIssue, + removeIssue, + removeIssueFromView, + archiveIssue, + updateFilters, + }), + [fetchIssues, createIssue, updateIssue, removeIssue, removeIssueFromView, archiveIssue, updateFilters] + ); +}; + +const useModuleIssueActions = () => { + const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); + + const { + router: { workspaceSlug, moduleId }, + } = useApplication(); + + const fetchIssues = useCallback( + async (projectId: string, loadType: TLoader) => { + if (!moduleId || !workspaceSlug) return; + return await issues.fetchIssues(workspaceSlug, projectId, loadType, moduleId); + }, + [issues.fetchIssues, moduleId, workspaceSlug] + ); + const createIssue = useCallback( + async (projectId: string, data: Partial) => { + if (!moduleId || !workspaceSlug) return; + return await issues.createIssue(workspaceSlug, projectId, data, moduleId); + }, + [issues.createIssue, moduleId, workspaceSlug] + ); + const updateIssue = useCallback( + async (projectId: string, issueId: string, data: Partial) => { + if (!moduleId || !workspaceSlug) return; + return await issues.updateIssue(workspaceSlug, projectId, issueId, data, moduleId); + }, + [issues.updateIssue, moduleId, workspaceSlug] + ); + const removeIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!moduleId || !workspaceSlug) return; + return await issues.removeIssue(workspaceSlug, projectId, issueId, moduleId); + }, + [issues.removeIssue, moduleId, workspaceSlug] + ); + const removeIssueFromView = useCallback( + async (projectId: string, issueId: string) => { + if (!moduleId || !workspaceSlug) return; + return await issues.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); + }, + [issues.removeIssueFromModule, moduleId, workspaceSlug] + ); + const archiveIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!moduleId || !workspaceSlug) return; + return await issues.archiveIssue(workspaceSlug, projectId, issueId, moduleId); + }, + [issues.archiveIssue, moduleId, workspaceSlug] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + if (!moduleId || !workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, moduleId); + }, + [issuesFilter.updateFilters, moduleId] + ); + + return useMemo( + () => ({ + fetchIssues, + createIssue, + updateIssue, + removeIssue, + removeIssueFromView, + archiveIssue, + updateFilters, + }), + [fetchIssues, createIssue, updateIssue, removeIssue, removeIssueFromView, archiveIssue, updateFilters] + ); +}; + +const useProfileIssueActions = () => { + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE); + + const { + router: { workspaceSlug, userId }, + } = useApplication(); + + const fetchIssues = useCallback( + async (projectId: string, loadType: TLoader) => { + if (!userId || !workspaceSlug) return; + return await issues.fetchIssues(workspaceSlug, projectId, loadType, userId); + }, + [issues.fetchIssues, userId, workspaceSlug] + ); + const createIssue = useCallback( + async (projectId: string, data: Partial) => { + if (!userId || !workspaceSlug) return; + return await issues.createIssue(workspaceSlug, projectId, data, userId); + }, + [issues.createIssue, userId, workspaceSlug] + ); + const updateIssue = useCallback( + async (projectId: string, issueId: string, data: Partial) => { + if (!userId || !workspaceSlug) return; + return await issues.updateIssue(workspaceSlug, projectId, issueId, data, userId); + }, + [issues.updateIssue, userId, workspaceSlug] + ); + const removeIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!userId || !workspaceSlug) return; + return await issues.removeIssue(workspaceSlug, projectId, issueId, userId); + }, + [issues.removeIssue, userId, workspaceSlug] + ); + const archiveIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!userId || !workspaceSlug) return; + return await issues.archiveIssue(workspaceSlug, projectId, issueId, userId); + }, + [issues.archiveIssue, userId, workspaceSlug] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + if (!userId || !workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, userId); + }, + [issuesFilter.updateFilters, userId, workspaceSlug] + ); + + return useMemo( + () => ({ + fetchIssues, + createIssue, + updateIssue, + removeIssue, + archiveIssue, + updateFilters, + }), + [fetchIssues, createIssue, updateIssue, removeIssue, archiveIssue, updateFilters] + ); +}; + +const useProjectViewIssueActions = () => { + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); + + const { + router: { workspaceSlug, viewId }, + } = useApplication(); + + const fetchIssues = useCallback( + async (projectId: string, loadType: TLoader) => { + if (!viewId || !workspaceSlug) return; + return await issues.fetchIssues(workspaceSlug, projectId, loadType, viewId); + }, + [issues.fetchIssues, viewId, workspaceSlug] + ); + const createIssue = useCallback( + async (projectId: string, data: Partial) => { + if (!viewId || !workspaceSlug) return; + return await issues.createIssue(workspaceSlug, projectId, data, viewId); + }, + [issues.createIssue, viewId, workspaceSlug] + ); + const updateIssue = useCallback( + async (projectId: string, issueId: string, data: Partial) => { + if (!viewId || !workspaceSlug) return; + return await issues.updateIssue(workspaceSlug, projectId, issueId, data, viewId); + }, + [issues.updateIssue, viewId, workspaceSlug] + ); + const removeIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!viewId || !workspaceSlug) return; + return await issues.removeIssue(workspaceSlug, projectId, issueId, viewId); + }, + [issues.removeIssue, viewId, workspaceSlug] + ); + const archiveIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!viewId || !workspaceSlug) return; + return await issues.archiveIssue(workspaceSlug, projectId, issueId, viewId); + }, + [issues.archiveIssue, viewId, workspaceSlug] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + if (!viewId || !workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, viewId); + }, + [issuesFilter.updateFilters, viewId, workspaceSlug] + ); + + return useMemo( + () => ({ + fetchIssues, + createIssue, + updateIssue, + removeIssue, + archiveIssue, + updateFilters, + }), + [fetchIssues, createIssue, updateIssue, removeIssue, archiveIssue, updateFilters] + ); +}; + +const useDraftIssueActions = () => { + const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); + + const { + router: { workspaceSlug }, + } = useApplication(); + + const fetchIssues = useCallback( + async (projectId: string, loadType: TLoader) => { + if (!workspaceSlug) return; + return await issues.fetchIssues(workspaceSlug, projectId, loadType); + }, + [issues.fetchIssues, workspaceSlug] + ); + const createIssue = useCallback( + async (projectId: string, data: Partial) => { + if (!workspaceSlug) return; + return await issues.createIssue(workspaceSlug, projectId, data); + }, + [issues.createIssue, workspaceSlug] + ); + const updateIssue = useCallback( + async (projectId: string, issueId: string, data: Partial) => { + if (!workspaceSlug) return; + return await issues.updateIssue(workspaceSlug, projectId, issueId, data); + }, + [issues.updateIssue, workspaceSlug] + ); + const removeIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!workspaceSlug) return; + return await issues.removeIssue(workspaceSlug, projectId, issueId); + }, + [issues.removeIssue, workspaceSlug] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + if (!workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters); + }, + [issuesFilter.updateFilters] + ); + + return useMemo( + () => ({ + fetchIssues, + createIssue, + updateIssue, + removeIssue, + updateFilters, + }), + [fetchIssues, createIssue, updateIssue, removeIssue, updateFilters] + ); +}; + +const useArchivedIssueActions = () => { + const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); + + const { + router: { workspaceSlug }, + } = useApplication(); + + const fetchIssues = useCallback( + async (projectId: string, loadType: TLoader) => { + if (!workspaceSlug) return; + return await issues.fetchIssues(workspaceSlug, projectId, loadType); + }, + [issues.fetchIssues] + ); + const removeIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!workspaceSlug) return; + return await issues.removeIssue(workspaceSlug, projectId, issueId); + }, + [issues.removeIssue] + ); + const restoreIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!workspaceSlug) return; + return await issues.restoreIssue(workspaceSlug, projectId, issueId); + }, + [issues.restoreIssue] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + if (!workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters); + }, + [issuesFilter.updateFilters] + ); + + return useMemo( + () => ({ + fetchIssues, + removeIssue, + restoreIssue, + updateFilters, + }), + [fetchIssues, removeIssue, restoreIssue, updateFilters] + ); +}; + +const useGlobalIssueActions = () => { + const { issues, issuesFilter } = useIssues(EIssuesStoreType.GLOBAL); + + const { + router: { workspaceSlug, globalViewId }, + } = useApplication(); + const createIssue = useCallback( + async (projectId: string, data: Partial) => { + if (!globalViewId || !workspaceSlug) return; + return await issues.createIssue(workspaceSlug, projectId, data, globalViewId); + }, + [issues.createIssue, globalViewId, workspaceSlug] + ); + const updateIssue = useCallback( + async (projectId: string, issueId: string, data: Partial) => { + if (!globalViewId || !workspaceSlug) return; + return await issues.updateIssue(workspaceSlug, projectId, issueId, data, globalViewId); + }, + [issues.updateIssue, globalViewId, workspaceSlug] + ); + const removeIssue = useCallback( + async (projectId: string, issueId: string) => { + if (!globalViewId || !workspaceSlug) return; + return await issues.removeIssue(workspaceSlug, projectId, issueId, globalViewId); + }, + [issues.removeIssue, globalViewId, workspaceSlug] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + if (!globalViewId || !workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, globalViewId); + }, + [issuesFilter.updateFilters, globalViewId, workspaceSlug] + ); + + return useMemo( + () => ({ + createIssue, + updateIssue, + removeIssue, + updateFilters, + }), + [createIssue, updateIssue, removeIssue, updateFilters] + ); +}; diff --git a/web/hooks/use-toast.tsx b/web/hooks/use-toast.tsx deleted file mode 100644 index 6de3c104c..000000000 --- a/web/hooks/use-toast.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext } from "react"; -import { toastContext } from "contexts/toast.context"; - -const useToast = () => { - const toastContextData = useContext(toastContext); - return toastContextData; -}; - -export default useToast; diff --git a/web/hooks/use-user-notifications.tsx b/web/hooks/use-user-notifications.tsx index 17a2c63dc..41bb6cbfd 100644 --- a/web/hooks/use-user-notifications.tsx +++ b/web/hooks/use-user-notifications.tsx @@ -4,13 +4,13 @@ import { useRouter } from "next/router"; import useSWR from "swr"; import useSWRInfinite from "swr/infinite"; // services -import { NotificationService } from "services/notification.service"; -// hooks -import useToast from "./use-toast"; -// fetch-keys import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constants/fetch-keys"; +import { NotificationService } from "services/notification.service"; +// fetch-keys // type import type { NotificationType, NotificationCount, IMarkAllAsReadPayload } from "@plane/types"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; const PER_PAGE = 30; @@ -20,8 +20,6 @@ const useUserNotification = () => { const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const [snoozed, setSnoozed] = useState(false); const [archived, setArchived] = useState(false); const [readNotification, setReadNotification] = useState(false); @@ -265,15 +263,15 @@ const useUserNotification = () => { await userNotificationServices .markAllNotificationsAsRead(workspaceSlug.toString(), markAsReadParams) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "All Notifications marked as read.", }); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/hooks/use-user.tsx b/web/hooks/use-user.tsx index 357579026..ffe6c963b 100644 --- a/web/hooks/use-user.tsx +++ b/web/hooks/use-user.tsx @@ -2,9 +2,9 @@ import { useEffect } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services +import { CURRENT_USER } from "constants/fetch-keys"; import { UserService } from "services/user.service"; // constants -import { CURRENT_USER } from "constants/fetch-keys"; // types import type { IUser } from "@plane/types"; diff --git a/web/layouts/admin-layout/header.tsx b/web/layouts/admin-layout/header.tsx index 2607fe91d..e12875d86 100644 --- a/web/layouts/admin-layout/header.tsx +++ b/web/layouts/admin-layout/header.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; // mobx import { observer } from "mobx-react-lite"; // ui +import { Settings } from "lucide-react"; import { Breadcrumbs } from "@plane/ui"; // icons -import { Settings } from "lucide-react"; import { BreadcrumbLink } from "components/common"; export interface IInstanceAdminHeader { diff --git a/web/layouts/admin-layout/layout.tsx b/web/layouts/admin-layout/layout.tsx index 2dbcdf1f5..bd53fc060 100644 --- a/web/layouts/admin-layout/layout.tsx +++ b/web/layouts/admin-layout/layout.tsx @@ -1,13 +1,13 @@ import { FC, ReactNode } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { InstanceSetupView } from "components/instance"; import { useApplication } from "hooks/store"; // layouts import { AdminAuthWrapper, UserAuthWrapper } from "layouts/auth-layout"; // components -import { InstanceAdminSidebar } from "./sidebar"; import { InstanceAdminHeader } from "./header"; -import { InstanceSetupView } from "components/instance"; +import { InstanceAdminSidebar } from "./sidebar"; export interface IInstanceAdminLayout { children: ReactNode; diff --git a/web/layouts/admin-layout/sidebar.tsx b/web/layouts/admin-layout/sidebar.tsx index efd3cfc76..2af3f0982 100644 --- a/web/layouts/admin-layout/sidebar.tsx +++ b/web/layouts/admin-layout/sidebar.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { InstanceAdminSidebarMenu, InstanceHelpSection, InstanceSidebarDropdown } from "components/instance"; import { useApplication } from "hooks/store"; // components -import { InstanceAdminSidebarMenu, InstanceHelpSection, InstanceSidebarDropdown } from "components/instance"; export interface IInstanceAdminSidebar {} diff --git a/web/layouts/app-layout/layout.tsx b/web/layouts/app-layout/layout.tsx index 2a788e761..dd1df164f 100644 --- a/web/layouts/app-layout/layout.tsx +++ b/web/layouts/app-layout/layout.tsx @@ -1,15 +1,13 @@ import { FC, ReactNode } from "react"; // layouts +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; +import { CommandPalette } from "components/command-palette"; +import { EIssuesStoreType } from "constants/issue"; +import { useIssues } from "hooks/store/use-issues"; import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "layouts/auth-layout"; // components -import { CommandPalette } from "components/command-palette"; import { AppSidebar } from "./sidebar"; -import { observer } from "mobx-react-lite"; - -// FIXME: remove this later -import { useIssues } from "hooks/store/use-issues"; -import { EIssuesStoreType } from "constants/issue"; -import useSWR from "swr"; export interface IAppLayout { children: ReactNode; @@ -20,22 +18,6 @@ export interface IAppLayout { export const AppLayout: FC = observer((props) => { const { children, header, withProjectWrapper = false } = props; - const workspaceSlug = "plane-demo"; - const projectId = "b16907a9-a55f-4f5b-b05e-7065a0869ba6"; - - const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); - - useSWR( - workspaceSlug && projectId ? `PROJECT_ARCHIVED_ISSUES_V3_${workspaceSlug}_${projectId}` : null, - async () => { - if (workspaceSlug && projectId) { - await issuesFilter?.fetchFilters(workspaceSlug, projectId); - // await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader"); - } - }, - { revalidateIfStale: false, revalidateOnFocus: false } - ); - return ( <> diff --git a/web/layouts/app-layout/sidebar.tsx b/web/layouts/app-layout/sidebar.tsx index 8477525d4..6ff6f01d7 100644 --- a/web/layouts/app-layout/sidebar.tsx +++ b/web/layouts/app-layout/sidebar.tsx @@ -1,13 +1,13 @@ import { FC, useRef } from "react"; import { observer } from "mobx-react-lite"; // components +import { ProjectSidebarList } from "components/project"; import { WorkspaceHelpSection, WorkspaceSidebarDropdown, WorkspaceSidebarMenu, WorkspaceSidebarQuickAction, } from "components/workspace"; -import { ProjectSidebarList } from "components/project"; // hooks import { useApplication } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -20,9 +20,9 @@ export const AppSidebar: FC = observer(() => { const ref = useRef(null); useOutsideClickDetector(ref, () => { - if (themStore.mobileSidebarCollapsed === false) { + if (themStore.sidebarCollapsed === false) { if (window.innerWidth < 768) { - themStore.toggleMobileSidebar(); + themStore.toggleSidebar(); } } }); @@ -31,8 +31,8 @@ export const AppSidebar: FC = observer(() => {
{header} -
{children}
+
+ {children} +
diff --git a/web/layouts/settings-layout/profile/preferences/sidebar.tsx b/web/layouts/settings-layout/profile/preferences/sidebar.tsx index 7f43f3cad..27b28905b 100644 --- a/web/layouts/settings-layout/profile/preferences/sidebar.tsx +++ b/web/layouts/settings-layout/profile/preferences/sidebar.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; export const ProfilePreferenceSettingsSidebar = () => { const router = useRouter(); @@ -9,15 +9,15 @@ export const ProfilePreferenceSettingsSidebar = () => { label: string; href: string; }> = [ - { - label: "Theme", - href: `/profile/preferences/theme`, - }, - { - label: "Email", - href: `/profile/preferences/email`, - }, - ]; + { + label: "Theme", + href: `/profile/preferences/theme`, + }, + { + label: "Email", + href: `/profile/preferences/email`, + }, + ]; return (
@@ -26,10 +26,11 @@ export const ProfilePreferenceSettingsSidebar = () => { {profilePreferenceLinks.map((link) => (
{link.label}
diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index fc0bf7435..1bae51d8b 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -1,15 +1,14 @@ import { useEffect, useRef, useState } from "react"; -import { mutate } from "swr"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; +import { mutate } from "swr"; import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react"; // hooks import { useApplication, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // ui -import { Tooltip } from "@plane/ui"; +import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { PROFILE_ACTION_LINKS } from "constants/profile"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -36,11 +35,9 @@ export const ProfileLayoutSidebar = observer(() => { const router = useRouter(); // next themes const { setTheme } = useTheme(); - // toast - const { setToastAlert } = useToast(); // store hooks const { - theme: { sidebarCollapsed, toggleSidebar, toggleMobileSidebar }, + theme: { sidebarCollapsed, toggleSidebar }, } = useApplication(); const { currentUser, currentUserSettings, signOut } = useUser(); const { workspaces } = useWorkspace(); @@ -78,7 +75,7 @@ export const ProfileLayoutSidebar = observer(() => { const handleItemClick = () => { if (window.innerWidth < 768) { - toggleMobileSidebar(); + toggleSidebar(); } }; @@ -92,8 +89,8 @@ export const ProfileLayoutSidebar = observer(() => { router.push("/"); }) .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) @@ -113,8 +110,9 @@ export const ProfileLayoutSidebar = observer(() => {
@@ -128,7 +126,7 @@ export const ProfileLayoutSidebar = observer(() => { {!sidebarCollapsed && (
Your account
)} -
+
{PROFILE_ACTION_LINKS.map((link) => { if (link.key === "change-password" && currentUser?.is_password_autoset) return null; @@ -136,10 +134,11 @@ export const ProfileLayoutSidebar = observer(() => {
{} {!sidebarCollapsed && link.label} @@ -155,22 +154,25 @@ export const ProfileLayoutSidebar = observer(() => {
Workspaces
)} {workspacesList && workspacesList.length > 0 && ( -
+
{workspacesList.map((workspace) => ( {workspace?.logo && workspace.logo !== "" ? ( {
{} {!sidebarCollapsed && link.label} @@ -208,8 +211,9 @@ export const ProfileLayoutSidebar = observer(() => {
); }); diff --git a/web/layouts/settings-layout/project/sidebar.tsx b/web/layouts/settings-layout/project/sidebar.tsx index 054add4ee..628c2a854 100644 --- a/web/layouts/settings-layout/project/sidebar.tsx +++ b/web/layouts/settings-layout/project/sidebar.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // ui import { Loader } from "@plane/ui"; // hooks +import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; import { useUser } from "hooks/store"; // constants -import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; export const ProjectSettingsSidebar = () => { const router = useRouter(); @@ -24,8 +24,8 @@ export const ProjectSettingsSidebar = () => {
SETTINGS - {[...Array(8)].map(() => ( - + {[...Array(8)].map((index) => ( + ))}
diff --git a/web/layouts/settings-layout/workspace/layout.tsx b/web/layouts/settings-layout/workspace/layout.tsx index 4ee0f1e33..3d5d057be 100644 --- a/web/layouts/settings-layout/workspace/layout.tsx +++ b/web/layouts/settings-layout/workspace/layout.tsx @@ -10,11 +10,11 @@ export const WorkspaceSettingLayout: FC = (props) => { const { children } = props; return ( -
+
-
+
{children}
diff --git a/web/layouts/settings-layout/workspace/sidebar.tsx b/web/layouts/settings-layout/workspace/sidebar.tsx index c8d4718c7..f5177139b 100644 --- a/web/layouts/settings-layout/workspace/sidebar.tsx +++ b/web/layouts/settings-layout/workspace/sidebar.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks +import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; import { useUser } from "hooks/store"; // constants -import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_LINKS } from "constants/workspace"; export const WorkspaceSettingsSidebar = () => { // router diff --git a/web/layouts/user-profile-layout/layout.tsx b/web/layouts/user-profile-layout/layout.tsx index 60c17d8d4..fcabf5f49 100644 --- a/web/layouts/user-profile-layout/layout.tsx +++ b/web/layouts/user-profile-layout/layout.tsx @@ -1,9 +1,11 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// hooks -import { useUser } from "hooks/store"; +import { useRouter } from "next/router"; // components import { ProfileNavbar, ProfileSidebar } from "components/profile"; +// hooks +import { useUser } from "hooks/store"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; type Props = { children: React.ReactNode; @@ -11,27 +13,25 @@ type Props = { showProfileIssuesFilter?: boolean; }; -const AUTHORIZED_ROLES = [20, 15, 10]; +const AUTHORIZED_ROLES = [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.VIEWER]; export const ProfileAuthWrapper: React.FC = observer((props) => { const { children, className, showProfileIssuesFilter } = props; + // router const router = useRouter(); - + // store hooks const { membership: { currentWorkspaceRole }, } = useUser(); - - if (!currentWorkspaceRole) return null; - - const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole); - + // derived values + const isAuthorized = currentWorkspaceRole && AUTHORIZED_ROLES.includes(currentWorkspaceRole); const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed"); return (
- + {isAuthorized || !isAuthorizedPath ? (
{children}
) : ( diff --git a/web/lib/app-provider.tsx b/web/lib/app-provider.tsx index 9c06947af..8fcb61744 100644 --- a/web/lib/app-provider.tsx +++ b/web/lib/app-provider.tsx @@ -1,25 +1,24 @@ import { FC, ReactNode } from "react"; +import { observer } from "mobx-react-lite"; import dynamic from "next/dynamic"; import Router from "next/router"; +import { useTheme } from "next-themes"; import NProgress from "nprogress"; -import { observer } from "mobx-react-lite"; -import { ThemeProvider } from "next-themes"; -// hooks -import { useApplication, useUser, useWorkspace } from "hooks/store"; -// constants -import { THEMES } from "constants/themes"; -// layouts -import InstanceLayout from "layouts/instance-layout"; -// contexts -import { ToastContextProvider } from "contexts/toast.context"; import { SWRConfig } from "swr"; +// ui +import { Toast } from "@plane/ui"; // constants import { SWR_CONFIG } from "constants/swr-config"; +//helpers +import { resolveGeneralTheme } from "helpers/theme.helper"; +// hooks +import { useApplication, useUser, useWorkspace } from "hooks/store"; +// layouts +import InstanceLayout from "layouts/instance-layout"; // dynamic imports const StoreWrapper = dynamic(() => import("lib/wrappers/store-wrapper"), { ssr: false }); const PostHogProvider = dynamic(() => import("lib/posthog-provider"), { ssr: false }); const CrispWrapper = dynamic(() => import("lib/wrappers/crisp-wrapper"), { ssr: false }); - // nprogress NProgress.configure({ showSpinner: false }); Router.events.on("routeChangeStart", NProgress.start); @@ -41,27 +40,29 @@ export const AppProvider: FC = observer((props) => { const { config: { envConfig }, } = useApplication(); + // themes + const { resolvedTheme } = useTheme(); return ( - - - - - - - {children} - - - - - - + <> + {/* TODO: Need to handle custom themes for toast */} + + + + + + {children} + + + + + ); }); diff --git a/web/lib/local-storage.ts b/web/lib/local-storage.ts index e0d77dc51..ab84b358f 100644 --- a/web/lib/local-storage.ts +++ b/web/lib/local-storage.ts @@ -3,15 +3,15 @@ import isEmpty from "lodash/isEmpty"; export const storage = { set: (key: string, value: object | string | boolean): void => { if (typeof window === undefined || typeof window === "undefined" || !key || !value) return undefined; - const _value: string | undefined = value + const tempValue: string | undefined = value ? ["string", "boolean"].includes(typeof value) ? value.toString() : isEmpty(value) - ? undefined - : JSON.stringify(value) + ? undefined + : JSON.stringify(value) : undefined; - if (!_value) return undefined; - window.localStorage.setItem(key, _value); + if (!tempValue) return undefined; + window.localStorage.setItem(key, tempValue); }, get: (key: string): string | undefined => { diff --git a/web/lib/posthog-provider.tsx b/web/lib/posthog-provider.tsx index e8c1b7899..80391ba95 100644 --- a/web/lib/posthog-provider.tsx +++ b/web/lib/posthog-provider.tsx @@ -2,12 +2,12 @@ import { FC, ReactNode, useEffect, useState } from "react"; import { useRouter } from "next/router"; import posthog from "posthog-js"; import { PostHogProvider as PHProvider } from "posthog-js/react"; -// mobx store provider -import { IUser } from "@plane/types"; -// helpers -import { getUserRole } from "helpers/user.helper"; // constants import { GROUP_WORKSPACE } from "constants/event-tracker"; +// helpers +import { getUserRole } from "helpers/user.helper"; +// types +import { IUser } from "@plane/types"; export interface IPosthogWrapper { children: ReactNode; @@ -45,6 +45,7 @@ const PostHogProvider: FC = (props) => { if (posthogAPIKey && posthogHost) { posthog.init(posthogAPIKey, { api_host: posthogHost || "https://app.posthog.com", + debug: process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "1", // Debug mode based on the environment variable autocapture: false, capture_pageview: false, // Disable automatic pageview capture, as we capture manually }); @@ -58,7 +59,7 @@ const PostHogProvider: FC = (props) => { posthog?.identify(user.email); posthog?.group(GROUP_WORKSPACE, currentWorkspaceId); } - }, [currentWorkspaceId, user]); + }, [currentWorkspaceId, lastWorkspaceId, user]); useEffect(() => { // Track page views diff --git a/web/lib/types.d.ts b/web/lib/types.d.ts index 2b03f6975..8dac1ff82 100644 --- a/web/lib/types.d.ts +++ b/web/lib/types.d.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/ban-types export type NextPageWithLayout

= NextPage & { getLayout?: (page: ReactElement) => ReactNode; }; diff --git a/web/lib/wrappers/crisp-wrapper.tsx b/web/lib/wrappers/crisp-wrapper.tsx index beacf916b..d2771abd8 100644 --- a/web/lib/wrappers/crisp-wrapper.tsx +++ b/web/lib/wrappers/crisp-wrapper.tsx @@ -4,8 +4,8 @@ import { IUser } from "@plane/types"; declare global { interface Window { - $crisp: any; - CRISP_WEBSITE_ID: any; + $crisp: unknown[]; + CRISP_WEBSITE_ID: unknown; } } @@ -22,8 +22,8 @@ const CrispWrapper: FC = (props) => { window.$crisp = []; window.CRISP_WEBSITE_ID = process.env.NEXT_PUBLIC_CRISP_ID; (function () { - var d = document; - var s = d.createElement("script"); + const d = document; + const s = d.createElement("script"); s.src = "https://client.crisp.chat/l.js"; s.async = true; d.getElementsByTagName("head")[0].appendChild(s); diff --git a/web/lib/wrappers/store-wrapper.tsx b/web/lib/wrappers/store-wrapper.tsx index 83867f557..1890bba50 100644 --- a/web/lib/wrappers/store-wrapper.tsx +++ b/web/lib/wrappers/store-wrapper.tsx @@ -1,12 +1,12 @@ import { ReactNode, useEffect, useState, FC } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; +import { useRouter } from "next/router"; import { useTheme } from "next-themes"; -// hooks -import { useApplication, useUser } from "hooks/store"; +import useSWR from "swr"; // helpers import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; +// hooks +import { useApplication, useUser } from "hooks/store"; interface IStoreWrapper { children: ReactNode; @@ -15,7 +15,7 @@ interface IStoreWrapper { const StoreWrapper: FC = observer((props) => { const { children } = props; // states - const [dom, setDom] = useState(); + const [dom, setDom] = useState(); // router const router = useRouter(); // store hooks diff --git a/web/package.json b/web/package.json index af28cbb3d..99e351191 100644 --- a/web/package.json +++ b/web/package.json @@ -20,7 +20,6 @@ "@nivo/core": "0.80.0", "@nivo/legends": "0.80.0", "@nivo/line": "0.80.0", - "@nivo/marimekko": "0.80.0", "@nivo/pie": "0.80.0", "@nivo/scatterplot": "0.80.0", "@plane/document-editor": "*", @@ -71,11 +70,7 @@ "@types/react-color": "^3.0.6", "@types/react-dom": "^18.2.17", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.48.2", - "@typescript-eslint/parser": "^5.48.2", - "eslint": "^8.31.0", "eslint-config-custom": "*", - "eslint-config-next": "12.2.2", "prettier": "^2.8.7", "tailwind-config-custom": "*", "tsconfig": "*", diff --git a/web/pages/404.tsx b/web/pages/404.tsx index a73cd2074..639a77333 100644 --- a/web/pages/404.tsx +++ b/web/pages/404.tsx @@ -1,17 +1,17 @@ import React from "react"; -import Link from "next/link"; +import type { NextPage } from "next"; import Image from "next/image"; +import Link from "next/link"; // components +import { Button } from "@plane/ui"; import { PageHead } from "components/core"; // layouts import DefaultLayout from "layouts/default-layout"; // ui -import { Button } from "@plane/ui"; // images import Image404 from "public/404.svg"; // types -import type { NextPage } from "next"; const PageNotFound: NextPage = () => ( diff --git a/web/pages/[workspaceSlug]/active-cycles.tsx b/web/pages/[workspaceSlug]/active-cycles.tsx index f366ddbd6..b7e3b4100 100644 --- a/web/pages/[workspaceSlug]/active-cycles.tsx +++ b/web/pages/[workspaceSlug]/active-cycles.tsx @@ -5,11 +5,11 @@ import { PageHead } from "components/core"; import { WorkspaceActiveCycleHeader } from "components/headers"; import { WorkspaceActiveCyclesUpgrade } from "components/workspace"; // layouts +import { useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useWorkspace } from "hooks/store"; const WorkspaceActiveCyclesPage: NextPageWithLayout = observer(() => { const { currentWorkspace } = useWorkspace(); diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 31c396b54..c7ee67cab 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -1,44 +1,33 @@ import React, { Fragment, ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Tab } from "@headlessui/react"; -import { useTheme } from "next-themes"; // hooks -import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; +import { useApplication, useEventTracker, useProject, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; +import { PageHead } from "components/core"; +import { EmptyState } from "components/empty-state"; import { WorkspaceAnalyticsHeader } from "components/headers"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -// constants -import { ANALYTICS_TABS } from "constants/analytics"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; // type import { NextPageWithLayout } from "lib/types"; +// constants +import { ANALYTICS_TABS } from "constants/analytics"; +import { EmptyStateType } from "constants/empty-state"; const AnalyticsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { analytics_tab } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateProjectModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); const { workspaceProjectIds } = useProject(); const { currentWorkspace } = useWorkspace(); // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode); - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined; return ( @@ -79,22 +68,11 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {

) : ( { - setTrackElement("Analytics empty state"); - toggleCreateProjectModal(true); - }, + type={EmptyStateType.WORKSPACE_ANALYTICS} + primaryButtonOnClick={() => { + setTrackElement("Analytics empty state"); + toggleCreateProjectModal(true); }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/pages/[workspaceSlug]/index.tsx b/web/pages/[workspaceSlug]/index.tsx index 8a6782de8..0011e2619 100644 --- a/web/pages/[workspaceSlug]/index.tsx +++ b/web/pages/[workspaceSlug]/index.tsx @@ -1,15 +1,15 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts -import { AppLayout } from "layouts/app-layout"; // components import { PageHead } from "components/core"; -import { WorkspaceDashboardView } from "components/page-views"; import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard"; +import { WorkspaceDashboardView } from "components/page-views"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const WorkspacePage: NextPageWithLayout = observer(() => { const { currentWorkspace } = useWorkspace(); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx new file mode 100644 index 000000000..87029724e --- /dev/null +++ b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx @@ -0,0 +1,84 @@ +import { ReactElement, useState } from "react"; +import { observer } from "mobx-react"; +import { useRouter } from "next/router"; +// hooks +import { Button } from "@plane/ui"; +import { UserProfileHeader } from "components/headers"; +import { DownloadActivityButton, WorkspaceActivityListPage } from "components/profile"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { useUser } from "hooks/store"; +// layouts +import { AppLayout } from "layouts/app-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; +// components +// ui +// types +import { NextPageWithLayout } from "lib/types"; +// constants + +const PER_PAGE = 100; + +const ProfileActivityPage: NextPageWithLayout = observer(() => { + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + // router + const router = useRouter(); + const { userId } = router.query; + // store hooks + const { + currentUser, + membership: { currentWorkspaceRole }, + } = useUser(); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const activityPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + activityPages.push( + + ); + + const canDownloadActivity = + currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + + return ( +
+
+

Recent activity

+ {canDownloadActivity && } +
+
+ {activityPages} + {pageCount < totalPages && resultsCount !== 0 && ( +
+ +
+ )} +
+
+ ); +}); + +ProfileActivityPage.getLayout = function getLayout(page: ReactElement) { + return ( + }> + {page} + + ); +}; + +export default ProfileActivityPage; diff --git a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx index 1cef81e78..9d1dbf072 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx @@ -1,13 +1,13 @@ import React, { ReactElement } from "react"; // layouts +import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; +import { ProfileIssuesPage } from "components/profile/profile-issues"; import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { ProfileIssuesPage } from "components/profile/profile-issues"; const ProfileAssignedIssuesPage: NextPageWithLayout = () => ( <> diff --git a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx index 47a8445d7..105d9d309 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx @@ -2,14 +2,14 @@ import { ReactElement } from "react"; // store import { observer } from "mobx-react-lite"; // layouts +import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; +import { ProfileIssuesPage } from "components/profile/profile-issues"; import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { ProfileIssuesPage } from "components/profile/profile-issues"; const ProfileCreatedIssuesPage: NextPageWithLayout = () => ( <> diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index a4d1debe1..947da1369 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -2,13 +2,10 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // services -import { UserService } from "services/user.service"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; import { ProfileActivity, ProfilePriorityDistribution, @@ -17,11 +14,14 @@ import { ProfileWorkload, } from "components/profile"; // types -import { IUserStateDistribution, TStateGroups } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // constants import { USER_PROFILE_DATA } from "constants/fetch-keys"; import { GROUP_CHOICES } from "constants/project"; +import { AppLayout } from "layouts/app-layout"; +import { ProfileAuthWrapper } from "layouts/user-profile-layout"; +import { NextPageWithLayout } from "lib/types"; +import { UserService } from "services/user.service"; +import { IUserStateDistribution, TStateGroups } from "@plane/types"; // services const userService = new UserService(); @@ -45,7 +45,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => { return ( <> -
+
diff --git a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx index c05c39302..c81ed6918 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx @@ -2,14 +2,14 @@ import { ReactElement } from "react"; // store import { observer } from "mobx-react-lite"; // layouts +import { PageHead } from "components/core"; +import { UserProfileHeader } from "components/headers"; +import { ProfileIssuesPage } from "components/profile/profile-issues"; import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; // components -import { UserProfileHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; -import { ProfileIssuesPage } from "components/profile/profile-issues"; const ProfileSubscribedIssuesPage: NextPageWithLayout = () => ( <> diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index ee1be4ebb..0b1b238a9 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -3,7 +3,6 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react"; import useSWR from "swr"; // hooks -import useToast from "hooks/use-toast"; import { useIssueDetail, useIssues, useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; @@ -12,7 +11,7 @@ import { IssueDetailRoot } from "components/issues"; import { ProjectArchivedIssueDetailsHeader } from "components/headers"; import { PageHead } from "components/core"; // ui -import { ArchiveIcon, Button, Loader } from "@plane/ui"; +import { ArchiveIcon, Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // icons import { RotateCcw } from "lucide-react"; // types @@ -35,7 +34,6 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { const { issues: { restoreIssue }, } = useIssues(EIssuesStoreType.ARCHIVED); - const { setToastAlert } = useToast(); const { getProjectById } = useProject(); const { membership: { currentProjectRole }, @@ -66,20 +64,21 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { await restoreIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString()) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success", message: issue && - `${getProjectById(issue.project_id)?.identifier}-${ - issue?.sequence_id - } is restored successfully under the project ${getProjectById(issue.project_id)?.name}`, + `${getProjectById(issue.project_id) + ?.identifier}-${issue?.sequence_id} is restored successfully under the project ${getProjectById( + issue.project_id + )?.name}`, }); router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx index 34019c026..353f0a8b6 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // layouts +import { PageHead } from "components/core"; +import { ProjectArchivedIssuesHeader } from "components/headers"; +import { ArchivedIssueLayoutRoot } from "components/issues"; +import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // contexts -import { ArchivedIssueLayoutRoot } from "components/issues"; // components -import { ProjectArchivedIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useProject } from "hooks/store"; const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 7b5ec8833..6eaef6c0f 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -1,23 +1,23 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { EmptyState } from "components/common"; +import { PageHead } from "components/core"; +import { CycleDetailsSidebar } from "components/cycles"; +import { CycleIssuesHeader } from "components/headers"; +import { CycleLayoutRoot } from "components/issues/issue-layouts"; import { useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; -import { CycleIssuesHeader } from "components/headers"; -import { CycleDetailsSidebar } from "components/cycles"; -import { CycleLayoutRoot } from "components/issues/issue-layouts"; // ui -import { EmptyState } from "components/common"; // assets +import { NextPageWithLayout } from "lib/types"; import emptyCycle from "public/empty-state/cycle.svg"; // types -import { NextPageWithLayout } from "lib/types"; const CycleDetailPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 0f86089aa..a22e252f2 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,10 +1,9 @@ import { Fragment, useCallback, useState, ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Tab } from "@headlessui/react"; -import { useTheme } from "next-themes"; // hooks -import { useEventTracker, useCycle, useUser, useProject } from "hooks/store"; +import { useEventTracker, useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; @@ -12,28 +11,21 @@ import { AppLayout } from "layouts/app-layout"; import { PageHead } from "components/core"; import { CyclesHeader } from "components/headers"; import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; +import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // ui import { Tooltip } from "@plane/ui"; -import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // types -import { TCycleView, TCycleLayout } from "@plane/types"; import { NextPageWithLayout } from "lib/types"; +import { TCycleView, TCycleLayout } from "@plane/types"; // constants import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { const [createModal, setCreateModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { currentProjectCycleIds, loader } = useCycle(); const { getProjectById } = useProject(); // router @@ -43,10 +35,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode); const totalCycles = currentProjectCycleIds?.length ?? 0; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const project = projectId ? getProjectById(projectId?.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; @@ -89,22 +78,11 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { {totalCycles === 0 ? (
{ + setTrackElement("Cycle empty state"); + setCreateModal(true); }} - primaryButton={{ - text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text, - onClick: () => { - setTrackElement("Cycle empty state"); - setCreateModal(true); - }, - }} - size="lg" - disabled={!isEditingAllowed} />
) : ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx index bf11063c3..c506e55b0 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/draft-issues/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { X, PenSquare } from "lucide-react"; // layouts -import { AppLayout } from "layouts/app-layout"; // components -import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root"; import { PageHead } from "components/core"; import { ProjectDraftIssueHeader } from "components/headers"; +import { DraftIssueLayoutRoot } from "components/issues/issue-layouts/roots/draft-issue-layout-root"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; -import { observer } from "mobx-react"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const ProjectDraftIssuesPage: NextPageWithLayout = observer(() => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index de412c9d7..f8fb1aa47 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { PageHead } from "components/core"; +import { ProjectInboxHeader } from "components/headers"; +import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; +import { InboxLayoutLoader } from "components/ui"; import { useProject, useInboxIssues } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { InboxLayoutLoader } from "components/ui"; -import { PageHead } from "components/core"; -import { ProjectInboxHeader } from "components/headers"; -import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx index 1021ad102..c3d3f2e5a 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx @@ -1,15 +1,15 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { ProjectInboxHeader } from "components/headers"; +import { InboxLayoutLoader } from "components/ui"; import { useInbox, useProject } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // ui -import { InboxLayoutLoader } from "components/ui"; // components -import { ProjectInboxHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 6ff7d5aa5..54994ab6d 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -1,19 +1,19 @@ import React, { ReactElement, useEffect } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // layouts -import { AppLayout } from "layouts/app-layout"; -// components +import { Loader } from "@plane/ui"; import { PageHead } from "components/core"; +// components import { ProjectIssueDetailsHeader } from "components/headers"; import { IssueDetailRoot } from "components/issues"; // ui -import { Loader } from "@plane/ui"; // types -import { NextPageWithLayout } from "lib/types"; // store hooks import { useApplication, useIssueDetail, useProject } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const IssueDetailsPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx index 2aa9ab2e6..241af79c4 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import Head from "next/head"; import { useRouter } from "next/router"; -import { observer } from "mobx-react"; // components -import { ProjectLayoutRoot } from "components/issues"; +import { PageHead } from "components/core"; import { ProjectIssuesHeader } from "components/headers"; +import { ProjectLayoutRoot } from "components/issues"; // types +import { useProject } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; import { NextPageWithLayout } from "lib/types"; // layouts -import { AppLayout } from "layouts/app-layout"; // hooks -import { useProject } from "hooks/store"; -import { PageHead } from "components/core"; const ProjectIssuesPage: NextPageWithLayout = observer(() => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index afbd97b8e..e55eea170 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -1,22 +1,22 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { EmptyState } from "components/common"; +import { PageHead } from "components/core"; +import { ModuleIssuesHeader } from "components/headers"; +import { ModuleLayoutRoot } from "components/issues"; +import { ModuleDetailsSidebar } from "components/modules"; import { useModule, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { ModuleDetailsSidebar } from "components/modules"; -import { ModuleLayoutRoot } from "components/issues"; -import { ModuleIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; -import { EmptyState } from "components/common"; // assets +import { NextPageWithLayout } from "lib/types"; import emptyModule from "public/empty-state/module.svg"; // types -import { NextPageWithLayout } from "lib/types"; const ModuleIssuesPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 085f1e3c3..3648f5922 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; // layouts -import { AppLayout } from "layouts/app-layout"; // components import { PageHead } from "components/core"; -import { ModulesListView } from "components/modules"; import { ModulesListHeader } from "components/headers"; +import { ModulesListView } from "components/modules"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; -import { observer } from "mobx-react"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const ProjectModulesPage: NextPageWithLayout = observer(() => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index bee4fc9c7..16dba79b3 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -1,34 +1,33 @@ -import { Sparkle } from "lucide-react"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; -import { useRouter } from "next/router"; import { ReactElement, useEffect, useRef, useState } from "react"; +import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; +import useSWR from "swr"; +import { Sparkle } from "lucide-react"; // hooks -import { useApplication, usePage, useUser, useWorkspace } from "hooks/store"; -import useReloadConfirmations from "hooks/use-reload-confirmation"; -import useToast from "hooks/use-toast"; -// services -import { FileService } from "services/file.service"; -// layouts -import { AppLayout } from "layouts/app-layout"; -// components +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; import { GptAssistantPopover, PageHead } from "components/core"; import { PageDetailsHeader } from "components/headers/page-details"; +import { IssuePeekOverview } from "components/issues"; +import { EUserProjectRoles } from "constants/project"; +import { useApplication, usePage, useUser, useWorkspace } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-specific-pages"; +import useReloadConfirmations from "hooks/use-reload-confirmation"; +// services +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; +import { FileService } from "services/file.service"; +// layouts +// components // ui -import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; -import { Spinner } from "@plane/ui"; // assets // helpers // types import { IPage } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // fetch-keys // constants -import { EUserProjectRoles } from "constants/project"; -import { useProjectPages } from "hooks/store/use-project-specific-pages"; -import { IssuePeekOverview } from "components/issues"; // services const fileService = new FileService(); @@ -53,10 +52,8 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { currentUser, membership: { currentProjectRole }, } = useUser(); - // toast alert - const { setToastAlert } = useToast(); - const { handleSubmit, setValue, watch, getValues, control, reset } = useForm({ + const { handleSubmit, getValues, control, reset } = useForm({ defaultValues: { name: "", description_html: "" }, }); @@ -127,16 +124,13 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { const updatePage = async (formData: IPage) => { if (!workspaceSlug || !projectId || !pageId) return; - await updateDescriptionAction(formData.description_html); + updateDescriptionAction(formData.description_html); }; const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId || !pageId) return; - const newDescription = `${watch("description_html")}

${response}

`; - setValue("description_html", newDescription); - editorRef.current?.setEditorValue(newDescription); - updateDescriptionAction(newDescription); + editorRef.current?.setEditorValueAtCursorPosition(response); }; const actionCompleteAlert = ({ @@ -148,10 +142,10 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { message: string; type: "success" | "error" | "warning" | "info"; }) => { - setToastAlert({ + setToast({ title, message, - type, + type: type as TOAST_TYPE, }); }; @@ -314,7 +308,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { updatePageTitle={updatePageTitle} onActionCompleteHandler={actionCompleteAlert} customClassName="tracking-tight self-center h-full w-full right-[0.675rem]" - onChange={(_description_json: Object, description_html: string) => { + onChange={(_description_json: any, description_html: string) => { setShowAlert(true); onChange(description_html); handleSubmit(updatePage)(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index a8c85ef8d..d299c2182 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -1,12 +1,12 @@ import { useState, Fragment, ReactElement } from "react"; -import { useRouter } from "next/router"; -import dynamic from "next/dynamic"; -import { Tab } from "@headlessui/react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; +import dynamic from "next/dynamic"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +import { Tab } from "@headlessui/react"; // hooks import { useApplication, useEventTracker, useUser, useProject } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-page"; import useLocalStorage from "hooks/use-local-storage"; import useUserAuth from "hooks/use-user-auth"; import useSize from "hooks/use-window-size"; @@ -14,17 +14,15 @@ import useSize from "hooks/use-window-size"; import { AppLayout } from "layouts/app-layout"; // components import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { PagesHeader } from "components/headers"; import { PagesLoader } from "components/ui"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants import { PAGE_TABS_LIST } from "constants/page"; -import { useProjectPages } from "hooks/store/use-project-page"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PageHead } from "components/core"; +import { EmptyStateType } from "constants/empty-state"; const AllPagesList = dynamic(() => import("components/pages").then((a) => a.AllPagesList), { ssr: false, @@ -52,14 +50,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { const { workspaceSlug, projectId } = router.query; // states const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - currentUser, - currentUserLoader, - membership: { currentProjectRole }, - } = useUser(); + const { currentUser, currentUserLoader } = useUser(); const { commandPalette: { toggleCreatePageModal }, } = useApplication(); @@ -103,9 +95,6 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { }; // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; @@ -216,22 +205,11 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { ) : ( { - setTrackElement("Pages empty state"); - toggleCreatePageModal(true); - }, + type={EmptyStateType.PROJECT_PAGE} + primaryButtonOnClick={() => { + setTrackElement("Pages empty state"); + toggleCreatePageModal(true); }} - comicBox={{ - title: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.title, - description: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx index 8c4780cba..d6724c789 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx @@ -1,29 +1,30 @@ import React, { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // hooks -import { useProject, useUser } from "hooks/store"; -// layouts -import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/settings-layout"; -// hooks -import useToast from "hooks/use-toast"; -// components +import { TOAST_TYPE, setToast } from "@plane/ui"; import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; +// layouts +// ui +// components import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; +import { EUserProjectRoles } from "constants/project"; +import { useProject, useUser } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +// layouts +import { ProjectSettingLayout } from "layouts/settings-layout"; +// hooks +// components // types import { NextPageWithLayout } from "lib/types"; import { IProject } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; const AutomationSettingsPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // toast alert - const { setToastAlert } = useToast(); // store hooks const { membership: { currentProjectRole }, @@ -34,8 +35,8 @@ const AutomationSettingsPage: NextPageWithLayout = observer(() => { if (!workspaceSlug || !projectId || !projectDetails) return; await updateProject(workspaceSlug.toString(), projectId.toString(), formData).catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 3aea45adb..c1aea645f 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -1,18 +1,18 @@ import { ReactElement } from "react"; import { observer } from "mobx-react-lite"; // hooks +import { PageHead } from "components/core"; +import { EstimatesList } from "components/estimates"; +import { ProjectSettingHeader } from "components/headers"; +import { EUserProjectRoles } from "constants/project"; import { useUser, useProject } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { PageHead } from "components/core"; -import { ProjectSettingHeader } from "components/headers"; -import { EstimatesList } from "components/estimates"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserProjectRoles } from "constants/project"; const EstimatesSettingsPage: NextPageWithLayout = observer(() => { const { @@ -26,7 +26,7 @@ const EstimatesSettingsPage: NextPageWithLayout = observer(() => { return ( <> -
+
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index b618437ab..e36ebd9a8 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { PageHead } from "components/core"; +import { ProjectSettingHeader } from "components/headers"; +import { ProjectFeaturesList } from "components/project"; import { useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { PageHead } from "components/core"; -import { ProjectSettingHeader } from "components/headers"; -import { ProjectFeaturesList } from "components/project"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 347d64f84..037e47434 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -1,13 +1,8 @@ import { useState, ReactElement } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // hooks -import { useProject } from "hooks/store"; -// layouts -import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/settings-layout"; -// components import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { @@ -16,6 +11,11 @@ import { ProjectDetailsForm, ProjectDetailsFormLoader, } from "components/project"; +import { useProject } from "hooks/store"; +// layouts +import { AppLayout } from "layouts/app-layout"; +import { ProjectSettingLayout } from "layouts/settings-layout"; +// components // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index 06246f1c2..60e9ca61a 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -1,29 +1,28 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { useTheme } from "next-themes"; -import { observer } from "mobx-react"; // hooks -import { useUser } from "hooks/store"; +import { IntegrationsSettingsLoader } from "components/ui"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // services +import { NextPageWithLayout } from "lib/types"; import { IntegrationService } from "services/integrations"; import { ProjectService } from "services/project"; // components import { PageHead } from "components/core"; import { IntegrationCard } from "components/project"; import { ProjectSettingHeader } from "components/headers"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui -import { IntegrationsSettingsLoader } from "components/ui"; // types import { IProject } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // fetch-keys import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; +// constants +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -32,10 +31,6 @@ const projectService = new ProjectService(); const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); // fetch project details const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, @@ -47,16 +42,13 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { () => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null) ); // derived values - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode); const isAdmin = projectDetails?.member_role === 20; const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined; return ( <> -
+

Integrations

@@ -70,15 +62,8 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { ) : (
router.push(`/${workspaceSlug}/settings/integrations`), - }} - size="lg" - disabled={!isAdmin} + type={EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS} + primaryButtonLink={`/${workspaceSlug}/settings/integrations`} />
) diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index 3bb1c8c04..8b3758829 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts +import { PageHead } from "components/core"; +import { ProjectSettingHeader } from "components/headers"; +import { ProjectSettingsLabelList } from "components/labels"; +import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { PageHead } from "components/core"; -import { ProjectSettingsLabelList } from "components/labels"; -import { ProjectSettingHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useProject } from "hooks/store"; const LabelsSettingsPage: NextPageWithLayout = observer(() => { const { currentProjectDetails } = useProject(); @@ -19,7 +19,7 @@ const LabelsSettingsPage: NextPageWithLayout = observer(() => { return ( <> -
+
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index f74d464d5..551dde0c2 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/settings-layout"; // components import { PageHead } from "components/core"; import { ProjectSettingHeader } from "components/headers"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project"; // types -import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { ProjectSettingLayout } from "layouts/settings-layout"; +import { NextPageWithLayout } from "lib/types"; const MembersSettingsPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index 3fa9561a8..4a5c290d8 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -1,21 +1,34 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; // layout +import { PageHead } from "components/core"; +import { ProjectSettingHeader } from "components/headers"; +import { ProjectSettingStateList } from "components/states"; +import { useProject } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; // components -import { ProjectSettingStateList } from "components/states"; -import { ProjectSettingHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; +// hook -const StatesSettingsPage: NextPageWithLayout = () => ( -
-
-

States

-
- -
-); +const StatesSettingsPage: NextPageWithLayout = observer(() => { + // store + const { currentProjectDetails } = useProject(); + // derived values + const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined; + return ( + <> + +
+
+

States

+
+ +
+ + ); +}); StatesSettingsPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx index 2ac6b2e00..17ba29394 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx @@ -1,21 +1,21 @@ import { ReactElement } from "react"; +import { observer } from "mobx-react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react"; // hooks +import { EmptyState } from "components/common"; +import { PageHead } from "components/core"; +import { ProjectViewIssuesHeader } from "components/headers"; +import { ProjectViewLayoutRoot } from "components/issues"; import { useProject, useProjectView } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { ProjectViewLayoutRoot } from "components/issues"; -import { ProjectViewIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; // ui -import { EmptyState } from "components/common"; // assets +import { NextPageWithLayout } from "lib/types"; import emptyView from "public/empty-state/view.svg"; // types -import { NextPageWithLayout } from "lib/types"; const ProjectViewIssuesPage: NextPageWithLayout = observer(() => { // router diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 33be5d102..9864ef391 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -1,10 +1,10 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // components +import { PageHead } from "components/core"; import { ProjectViewsHeader } from "components/headers"; import { ProjectViewsList } from "components/views"; -import { PageHead } from "components/core"; // hooks import { useProject } from "hooks/store"; // layouts diff --git a/web/pages/[workspaceSlug]/projects/index.tsx b/web/pages/[workspaceSlug]/projects/index.tsx index 1a145a2d1..158e6577f 100644 --- a/web/pages/[workspaceSlug]/projects/index.tsx +++ b/web/pages/[workspaceSlug]/projects/index.tsx @@ -2,13 +2,13 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // components import { PageHead } from "components/core"; -import { ProjectCardList } from "components/project"; import { ProjectsHeader } from "components/headers"; +import { ProjectCardList } from "components/project"; // layouts +import { useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // type import { NextPageWithLayout } from "lib/types"; -import { useWorkspace } from "hooks/store"; const ProjectsPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/[workspaceSlug]/settings/api-tokens.tsx b/web/pages/[workspaceSlug]/settings/api-tokens.tsx index 1f203ff04..59c205968 100644 --- a/web/pages/[workspaceSlug]/settings/api-tokens.tsx +++ b/web/pages/[workspaceSlug]/settings/api-tokens.tsx @@ -1,29 +1,28 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; -import { useTheme } from "next-themes"; // store hooks import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component +import { APITokenSettingsLoader } from "components/ui"; import { WorkspaceSettingHeader } from "components/headers"; import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; +import { PageHead } from "components/core"; // ui import { Button } from "@plane/ui"; -import { APITokenSettingsLoader } from "components/ui"; // services +import { NextPageWithLayout } from "lib/types"; import { APITokenService } from "services/api_token.service"; // types -import { NextPageWithLayout } from "lib/types"; // constants import { API_TOKENS_LIST } from "constants/fetch-keys"; import { EUserWorkspaceRoles } from "constants/workspace"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PageHead } from "components/core"; +import { EmptyStateType } from "constants/empty-state"; const apiTokenService = new APITokenService(); @@ -33,12 +32,9 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { membership: { currentWorkspaceRole }, - currentUser, } = useUser(); const { currentWorkspace } = useWorkspace(); @@ -48,9 +44,6 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; if (!isAdmin) @@ -71,7 +64,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { <> setIsCreateTokenModalOpen(false)} /> -
+
{tokens.length > 0 ? ( <>
@@ -95,12 +88,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
- +
)} diff --git a/web/pages/[workspaceSlug]/settings/billing.tsx b/web/pages/[workspaceSlug]/settings/billing.tsx index f4f5d5397..bd1114f85 100644 --- a/web/pages/[workspaceSlug]/settings/billing.tsx +++ b/web/pages/[workspaceSlug]/settings/billing.tsx @@ -1,18 +1,18 @@ import { observer } from "mobx-react-lite"; // hooks +import { Button } from "@plane/ui"; +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component -import { WorkspaceSettingHeader } from "components/headers"; -import { PageHead } from "components/core"; // ui -import { Button } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const BillingSettingsPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/[workspaceSlug]/settings/exports.tsx b/web/pages/[workspaceSlug]/settings/exports.tsx index c124a6423..a6f958472 100644 --- a/web/pages/[workspaceSlug]/settings/exports.tsx +++ b/web/pages/[workspaceSlug]/settings/exports.tsx @@ -1,17 +1,17 @@ import { observer } from "mobx-react-lite"; // hooks +import { PageHead } from "components/core"; +import ExportGuide from "components/exporter/guide"; +import { WorkspaceSettingHeader } from "components/headers"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layout import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import ExportGuide from "components/exporter/guide"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const ExportsPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/[workspaceSlug]/settings/imports.tsx b/web/pages/[workspaceSlug]/settings/imports.tsx index 5178209d2..19eeeac66 100644 --- a/web/pages/[workspaceSlug]/settings/imports.tsx +++ b/web/pages/[workspaceSlug]/settings/imports.tsx @@ -1,17 +1,17 @@ import { observer } from "mobx-react-lite"; // hooks +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import IntegrationGuide from "components/integration/guide"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts -import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { AppLayout } from "layouts/app-layout"; +import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import IntegrationGuide from "components/integration/guide"; -import { WorkspaceSettingHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; const ImportsPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/[workspaceSlug]/settings/index.tsx b/web/pages/[workspaceSlug]/settings/index.tsx index 2924b13c4..37ce39335 100644 --- a/web/pages/[workspaceSlug]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/settings/index.tsx @@ -1,14 +1,14 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; // layouts +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { WorkspaceDetails } from "components/workspace"; +import { useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // hooks -import { useWorkspace } from "hooks/store"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { WorkspaceDetails } from "components/workspace"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/[workspaceSlug]/settings/integrations.tsx b/web/pages/[workspaceSlug]/settings/integrations.tsx index 500533877..0aa54f60a 100644 --- a/web/pages/[workspaceSlug]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/settings/integrations.tsx @@ -1,26 +1,26 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks -import { useUser, useWorkspace } from "hooks/store"; // services -import { IntegrationService } from "services/integrations"; // layouts -import { AppLayout } from "layouts/app-layout"; -import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { SingleIntegrationCard } from "components/integration"; -import { WorkspaceSettingHeader } from "components/headers"; import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { SingleIntegrationCard } from "components/integration"; // ui import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "components/ui"; // types -import { NextPageWithLayout } from "lib/types"; // fetch-keys import { APP_INTEGRATIONS } from "constants/fetch-keys"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; +import { useUser, useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { WorkspaceSettingLayout } from "layouts/settings-layout"; +import { NextPageWithLayout } from "lib/types"; +import { IntegrationService } from "services/integrations"; const integrationService = new IntegrationService(); diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index b8739ae77..e1be1d889 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -1,27 +1,26 @@ import { useState, ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Search } from "lucide-react"; // hooks +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; +import { MEMBER_INVITED } from "constants/event-tracker"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { getUserRole } from "helpers/user.helper"; import { useEventTracker, useMember, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; -import { PageHead } from "components/core"; // ui -import { Button } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWorkspaceBulkInviteFormData } from "@plane/types"; // helpers -import { getUserRole } from "helpers/user.helper"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { MEMBER_INVITED } from "constants/event-tracker"; const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { // states @@ -31,7 +30,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store hooks - const { captureEvent, setTrackElement } = useEventTracker(); + const { captureEvent } = useEventTracker(); const { membership: { currentWorkspaceRole }, } = useUser(); @@ -39,8 +38,6 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { workspace: { inviteMembersToWorkspace }, } = useMember(); const { currentWorkspace } = useWorkspace(); - // toast alert - const { setToastAlert } = useToast(); const handleWorkspaceInvite = (data: IWorkspaceBulkInviteFormData) => { if (!workspaceSlug) return; @@ -59,8 +56,8 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { state: "SUCCESS", element: "Workspace settings member page", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Invitations sent successfully.", }); @@ -77,8 +74,8 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { state: "FAILED", element: "Workspace settings member page", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: `${err.error ?? "Something went wrong. Please try again."}`, }); diff --git a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx index 60e65e905..263f90963 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/[webhookId].tsx @@ -1,20 +1,19 @@ import { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import useSWR from "swr"; // hooks +import { Spinner, TOAST_TYPE, setToast } from "@plane/ui"; + +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; -// hooks -import useToast from "hooks/use-toast"; // components -import { WorkspaceSettingHeader } from "components/headers"; -import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "components/web-hooks"; -import { PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { IWebhook } from "@plane/types"; @@ -31,8 +30,6 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { } = useUser(); const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); - // toast - const { setToastAlert } = useToast(); // TODO: fix this error // useEffect(() => { @@ -62,15 +59,15 @@ const WebhookDetailsPage: NextPageWithLayout = observer(() => { }; await updateWebhook(workspaceSlug.toString(), formData.id, payload) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Webhook updated successfully.", }); }) .catch((error) => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: error?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index 46c7e99cb..24dca325c 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -1,25 +1,24 @@ import React, { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components +import { PageHead } from "components/core"; import { WorkspaceSettingHeader } from "components/headers"; +import { WebhookSettingsLoader } from "components/ui"; import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui import { Button } from "@plane/ui"; -import { WebhookSettingsLoader } from "components/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PageHead } from "components/core"; +import { EmptyStateType } from "constants/empty-state"; const WebhooksListPage: NextPageWithLayout = observer(() => { // states @@ -27,12 +26,9 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // theme - const { resolvedTheme } = useTheme(); // mobx store const { membership: { currentWorkspaceRole }, - currentUser, } = useUser(); const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); @@ -44,10 +40,6 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["webhooks"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined; // clear secret key when modal is closed. @@ -70,7 +62,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { return ( <> -
+
{
- +
)} diff --git a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx index 85e907481..7d736e8f9 100644 --- a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx +++ b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx @@ -1,19 +1,19 @@ import { ReactElement } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // layouts +import { PageHead } from "components/core"; +import { GlobalIssuesHeader } from "components/headers"; +import { AllIssueLayoutRoot } from "components/issues"; +import { GlobalViewsHeader } from "components/workspace"; +import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; +import { useGlobalView, useWorkspace } from "hooks/store"; import { AppLayout } from "layouts/app-layout"; // hooks -import { useGlobalView, useWorkspace } from "hooks/store"; // components -import { GlobalViewsHeader } from "components/workspace"; -import { AllIssueLayoutRoot } from "components/issues"; -import { GlobalIssuesHeader } from "components/headers"; -import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants -import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; const GlobalViewIssuesPage: NextPageWithLayout = observer(() => { // router @@ -29,8 +29,8 @@ const GlobalViewIssuesPage: NextPageWithLayout = observer(() => { currentWorkspace?.name && defaultView?.label ? `${currentWorkspace?.name} - ${defaultView?.label}` : currentWorkspace?.name && globalViewDetails?.name - ? `${currentWorkspace?.name} - ${globalViewDetails?.name}` - : undefined; + ? `${currentWorkspace?.name} - ${globalViewDetails?.name}` + : undefined; return ( <> diff --git a/web/pages/[workspaceSlug]/workspace-views/index.tsx b/web/pages/[workspaceSlug]/workspace-views/index.tsx index 61fdcf058..ccd7ac485 100644 --- a/web/pages/[workspaceSlug]/workspace-views/index.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/index.tsx @@ -1,21 +1,21 @@ import React, { useState, ReactElement } from "react"; import { observer } from "mobx-react"; // layouts -import { AppLayout } from "layouts/app-layout"; // components -import { PageHead } from "components/core"; -import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace"; -import { GlobalIssuesHeader } from "components/headers"; // ui +import { Search } from "lucide-react"; import { Input } from "@plane/ui"; // icons -import { Search } from "lucide-react"; +import { PageHead } from "components/core"; +import { GlobalIssuesHeader } from "components/headers"; +import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace"; // types -import { NextPageWithLayout } from "lib/types"; // constants import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; // hooks import { useWorkspace } from "hooks/store"; +import { AppLayout } from "layouts/app-layout"; +import { NextPageWithLayout } from "lib/types"; const WorkspaceViewsPage: NextPageWithLayout = observer(() => { const [query, setQuery] = useState(""); diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 75023d36e..48cd5a80c 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -1,12 +1,15 @@ import { ReactElement } from "react"; import Head from "next/head"; import { AppProps } from "next/app"; +import { ThemeProvider } from "next-themes"; // styles import "styles/globals.css"; import "styles/command-pallette.css"; import "styles/nprogress.css"; +import "styles/emoji.css"; import "styles/react-day-picker.css"; // constants +import { THEMES } from "constants/themes"; import { SITE_TITLE } from "constants/seo-variables"; // mobx store provider import { StoreProvider } from "contexts/store-context"; @@ -29,7 +32,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { {SITE_TITLE} - {getLayout()} + + {getLayout()} + ); diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index cc0411068..ccaa34a40 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -1,5 +1,6 @@ import Document, { Html, Head, Main, NextScript } from "next/document"; // constants +import Script from "next/script"; import { SITE_NAME, SITE_DESCRIPTION, @@ -8,7 +9,6 @@ import { SITE_KEYWORDS, SITE_TITLE, } from "constants/seo-variables"; -import Script from "next/script"; class MyDocument extends Document { render() { diff --git a/web/pages/_error.tsx b/web/pages/_error.tsx index 11a7ee852..81e0daecd 100644 --- a/web/pages/_error.tsx +++ b/web/pages/_error.tsx @@ -3,13 +3,12 @@ import * as Sentry from "@sentry/nextjs"; import { useRouter } from "next/router"; // services -import { AuthService } from "services/auth.service"; -// hooks -import useToast from "hooks/use-toast"; -// layouts +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; + import DefaultLayout from "layouts/default-layout"; +import { AuthService } from "services/auth.service"; +// layouts // ui -import { Button } from "@plane/ui"; // services const authService = new AuthService(); @@ -17,14 +16,12 @@ const authService = new AuthService(); const CustomErrorComponent = () => { const router = useRouter(); - const { setToastAlert } = useToast(); - const handleSignOut = async () => { await authService .signOut() .catch(() => - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Failed to sign out. Please try again.", }) diff --git a/web/pages/accounts/forgot-password.tsx b/web/pages/accounts/forgot-password.tsx index 0eef16009..e167fc037 100644 --- a/web/pages/accounts/forgot-password.tsx +++ b/web/pages/accounts/forgot-password.tsx @@ -5,7 +5,6 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; import { useEventTracker } from "hooks/store"; // layouts @@ -14,7 +13,7 @@ import DefaultLayout from "layouts/default-layout"; import { LatestFeatureBlock } from "components/common"; import { PageHead } from "components/core"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // helpers @@ -40,8 +39,6 @@ const ForgotPasswordPage: NextPageWithLayout = () => { const { email } = router.query; // store hooks const { captureEvent } = useEventTracker(); - // toast - const { setToastAlert } = useToast(); // timer const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0); // form info @@ -65,8 +62,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => { captureEvent(FORGOT_PASS_LINK, { state: "SUCCESS", }); - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Email sent", message: "Check your inbox for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder.", @@ -77,8 +74,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => { captureEvent(FORGOT_PASS_LINK, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/pages/accounts/reset-password.tsx b/web/pages/accounts/reset-password.tsx index c848245ac..f7a49a19d 100644 --- a/web/pages/accounts/reset-password.tsx +++ b/web/pages/accounts/reset-password.tsx @@ -5,7 +5,6 @@ import { Controller, useForm } from "react-hook-form"; // services import { AuthService } from "services/auth.service"; // hooks -import useToast from "hooks/use-toast"; import useSignInRedirection from "hooks/use-sign-in-redirection"; import { useEventTracker } from "hooks/store"; // layouts @@ -14,7 +13,7 @@ import DefaultLayout from "layouts/default-layout"; import { LatestFeatureBlock } from "components/common"; import { PageHead } from "components/core"; // ui -import { Button, Input } from "@plane/ui"; +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // images import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // helpers @@ -47,8 +46,6 @@ const ResetPasswordPage: NextPageWithLayout = () => { const [showPassword, setShowPassword] = useState(false); // store hooks const { captureEvent } = useEventTracker(); - // toast - const { setToastAlert } = useToast(); // sign in redirection hook const { handleRedirection } = useSignInRedirection(); // form info @@ -82,8 +79,8 @@ const ResetPasswordPage: NextPageWithLayout = () => { captureEvent(NEW_PASS_CREATED, { state: "FAILED", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }); diff --git a/web/pages/accounts/sign-up.tsx b/web/pages/accounts/sign-up.tsx index cba9c0166..c40a1660b 100644 --- a/web/pages/accounts/sign-up.tsx +++ b/web/pages/accounts/sign-up.tsx @@ -1,19 +1,19 @@ import React from "react"; -import Image from "next/image"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; // hooks +import { Spinner } from "@plane/ui"; +import { SignUpRoot } from "components/account"; +import { PageHead } from "components/core"; import { useApplication, useUser } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; // components -import { SignUpRoot } from "components/account"; -import { PageHead } from "components/core"; // ui -import { Spinner } from "@plane/ui"; // assets +import { NextPageWithLayout } from "lib/types"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types -import { NextPageWithLayout } from "lib/types"; const SignUpPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/create-workspace.tsx b/web/pages/create-workspace.tsx index 952ed0b68..add8d6673 100644 --- a/web/pages/create-workspace.tsx +++ b/web/pages/create-workspace.tsx @@ -1,23 +1,23 @@ import React, { useState, ReactElement } from "react"; -import { useRouter } from "next/router"; -import Image from "next/image"; -import { useTheme } from "next-themes"; import { observer } from "mobx-react-lite"; +import Image from "next/image"; import Link from "next/link"; +import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; // hooks +import { PageHead } from "components/core"; +import { CreateWorkspaceForm } from "components/workspace"; import { useUser } from "hooks/store"; // layouts -import DefaultLayout from "layouts/default-layout"; import { UserAuthWrapper } from "layouts/auth-layout"; +import DefaultLayout from "layouts/default-layout"; // components -import { CreateWorkspaceForm } from "components/workspace"; -import { PageHead } from "components/core"; // images +import { NextPageWithLayout } from "lib/types"; import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; // types import { IWorkspace } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; const CreateWorkspacePage: NextPageWithLayout = observer(() => { // router @@ -66,7 +66,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
diff --git a/web/pages/god-mode/ai.tsx b/web/pages/god-mode/ai.tsx index b84e98098..35b652d9b 100644 --- a/web/pages/god-mode/ai.tsx +++ b/web/pages/god-mode/ai.tsx @@ -1,19 +1,19 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Lightbulb } from "lucide-react"; +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceAIForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // icons -import { Lightbulb } from "lucide-react"; // components -import { InstanceAIForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminAIPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/authorization.tsx b/web/pages/god-mode/authorization.tsx index e36a1a455..f4eeefc65 100644 --- a/web/pages/god-mode/authorization.tsx +++ b/web/pages/god-mode/authorization.tsx @@ -1,20 +1,19 @@ import { ReactElement, useState } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // layouts +import { Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; + +import { PageHead } from "components/core"; +import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; -// hooks -import useToast from "hooks/use-toast"; // ui -import { Loader, ToggleSwitch } from "@plane/ui"; // components -import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { // store @@ -24,9 +23,6 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); - // toast - const { setToastAlert } = useToast(); - // state const [isSubmitting, setIsSubmitting] = useState(false); @@ -46,18 +42,18 @@ const InstanceAdminAuthorizationPage: NextPageWithLayout = observer(() => { await updateInstanceConfigurations(payload) .then(() => { - setToastAlert({ + setToast({ title: "Success", - type: "success", + type: TOAST_TYPE.SUCCESS, message: "SSO and OAuth Settings updated successfully", }); setIsSubmitting(false); }) .catch((err) => { console.error(err); - setToastAlert({ + setToast({ title: "Error", - type: "error", + type: TOAST_TYPE.ERROR, message: "Failed to update SSO and OAuth Settings", }); setIsSubmitting(false); diff --git a/web/pages/god-mode/email.tsx b/web/pages/god-mode/email.tsx index 65889607f..0e4a594ce 100644 --- a/web/pages/god-mode/email.tsx +++ b/web/pages/god-mode/email.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceEmailForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // components -import { InstanceEmailForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminEmailPage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/image.tsx b/web/pages/god-mode/image.tsx index 349dccf4b..4c6abaa96 100644 --- a/web/pages/god-mode/image.tsx +++ b/web/pages/god-mode/image.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceImageConfigForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // components -import { InstanceImageConfigForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminImagePage: NextPageWithLayout = observer(() => { // store diff --git a/web/pages/god-mode/index.tsx b/web/pages/god-mode/index.tsx index a93abad31..a7cb29c05 100644 --- a/web/pages/god-mode/index.tsx +++ b/web/pages/god-mode/index.tsx @@ -1,17 +1,17 @@ import { ReactElement } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import useSWR from "swr"; // layouts +import { Loader } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InstanceGeneralForm } from "components/instance"; +import { useApplication } from "hooks/store"; import { InstanceAdminLayout } from "layouts/admin-layout"; // types import { NextPageWithLayout } from "lib/types"; // hooks -import { useApplication } from "hooks/store"; // ui -import { Loader } from "@plane/ui"; // components -import { InstanceGeneralForm } from "components/instance"; -import { PageHead } from "components/core"; const InstanceAdminPage: NextPageWithLayout = observer(() => { // store hooks diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 2f8b32394..d9e99811f 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,8 +1,8 @@ import { ReactElement } from "react"; // layouts +import { SignInView } from "components/page-views"; import DefaultLayout from "layouts/default-layout"; // components -import { SignInView } from "components/page-views"; // type import { NextPageWithLayout } from "lib/types"; diff --git a/web/pages/installations/[provider]/index.tsx b/web/pages/installations/[provider]/index.tsx index 85bf21539..052782dc5 100644 --- a/web/pages/installations/[provider]/index.tsx +++ b/web/pages/installations/[provider]/index.tsx @@ -1,11 +1,11 @@ import React, { useEffect, ReactElement } from "react"; import { useRouter } from "next/router"; // services -import { AppInstallationService } from "services/app_installation.service"; // ui import { Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; +import { AppInstallationService } from "services/app_installation.service"; // services const appInstallationService = new AppInstallationService(); diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx index b5acec196..7f976865b 100644 --- a/web/pages/invitations/index.tsx +++ b/web/pages/invitations/index.tsx @@ -11,12 +11,11 @@ import { WorkspaceService } from "services/workspace.service"; import { UserService } from "services/user.service"; // hooks import { useEventTracker, useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; // layouts import DefaultLayout from "layouts/default-layout"; import { UserAuthWrapper } from "layouts/auth-layout"; // ui -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // images import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; @@ -48,8 +47,6 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { const router = useRouter(); // next-themes const { theme } = useTheme(); - // toast alert - const { setToastAlert } = useToast(); const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS", () => workspaceService.userWorkspaceInvitations()); @@ -68,8 +65,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { const submitInvitations = () => { if (invitationsRespond.length === 0) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one invitation.", }); @@ -80,7 +77,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { workspaceService .joinWorkspaces({ invitations: invitationsRespond }) - .then((res) => { + .then(() => { mutate("USER_WORKSPACES"); const firstInviteId = invitationsRespond[0]; const invitation = invitations?.find((i) => i.id === firstInviteId); @@ -88,6 +85,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { joinWorkspaceMetricGroup(redirectWorkspace?.id); captureEvent(MEMBER_ACCEPTED, { member_id: invitation?.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain role: getUserRole(invitation?.role!), project_id: undefined, accepted_from: "App", @@ -101,8 +99,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { router.push(`/${redirectWorkspace?.slug}`); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong, Please try again.", }); @@ -116,8 +114,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { state: "FAILED", element: "Workspace invitations page", }); - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong, Please try again.", }); diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index 5b5b91280..2ebd61f3a 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -1,32 +1,32 @@ import { useEffect, useState, ReactElement } from "react"; -import Image from "next/image"; -import { useTheme } from "next-themes"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import useSWR from "swr"; -import { ChevronDown } from "lucide-react"; -import { Menu, Transition } from "@headlessui/react"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; import { Controller, useForm } from "react-hook-form"; +import useSWR from "swr"; +import { Menu, Transition } from "@headlessui/react"; +import { ChevronDown } from "lucide-react"; // hooks +import { Avatar, Spinner } from "@plane/ui"; +import { PageHead } from "components/core"; +import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding"; +import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker"; import { useEventTracker, useUser, useWorkspace } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services +import { UserAuthWrapper } from "layouts/auth-layout"; +import DefaultLayout from "layouts/default-layout"; +import { NextPageWithLayout } from "lib/types"; +import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import { WorkspaceService } from "services/workspace.service"; // layouts -import DefaultLayout from "layouts/default-layout"; -import { UserAuthWrapper } from "layouts/auth-layout"; // components -import { InviteMembers, JoinWorkspaces, UserDetails, SwitchOrDeleteAccountModal } from "components/onboarding"; -import { PageHead } from "components/core"; // ui -import { Avatar, Spinner } from "@plane/ui"; // images -import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types import { IUser, TOnboardingSteps } from "@plane/types"; -import { NextPageWithLayout } from "lib/types"; // constants -import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker"; // services const workspaceService = new WorkspaceService(); @@ -166,8 +166,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : currentUser?.email + ? value + : currentUser?.email } src={currentUser?.avatar} size={35} @@ -182,8 +182,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { {currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : null} + ? value + : null}

)} diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index 4460f2ec5..bda1295cf 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -1,191 +1,64 @@ -import { ReactElement } from "react"; -import useSWR from "swr"; -import Link from "next/link"; +import { ReactElement, useState } from "react"; import { observer } from "mobx-react"; //hooks -import { useApplication, useUser } from "hooks/store"; -// services -import { UserService } from "services/user.service"; +import { Button } from "@plane/ui"; +import { PageHead } from "components/core"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { ProfileActivityListPage } from "components/profile"; +import { useApplication } from "hooks/store"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components -import { ActivityIcon, ActivityMessage, IssueLink, PageHead } from "components/core"; -import { RichReadOnlyEditor } from "@plane/rich-text-editor"; -// icons -import { History, MessageSquare } from "lucide-react"; // ui -import { ActivitySettingsLoader } from "components/ui"; -// fetch-keys -import { USER_ACTIVITY } from "constants/fetch-keys"; -// helper -import { calculateTimeAgo } from "helpers/date-time.helper"; // type import { NextPageWithLayout } from "lib/types"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; -const userService = new UserService(); +const PER_PAGE = 100; const ProfileActivityPage: NextPageWithLayout = observer(() => { - const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); // store hooks - const { currentUser } = useUser(); const { theme: themeStore } = useApplication(); + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const activityPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + activityPages.push( + + ); + return ( <> -
+
themeStore.toggleSidebar()} />

Activity

- {userActivity ? ( -
-
    - {userActivity.results.map((activityItem: any) => { - if (activityItem.field === "comment") { - return ( -
    -
    -
    - {activityItem.field ? ( - activityItem.new_value === "restore" && ( - - ) - ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.display_name} - ) : ( -
    - {activityItem.actor_detail.display_name?.charAt(0)} -
    - )} - - - -
    -
    -
    -
    - {activityItem.actor_detail.is_bot - ? activityItem.actor_detail.first_name + " Bot" - : activityItem.actor_detail.display_name} -
    -

    - Commented {calculateTimeAgo(activityItem.created_at)} -

    -
    -
    - -
    -
    -
    -
    - ); - } - - const message = - activityItem.verb === "created" && - activityItem.field !== "cycles" && - activityItem.field !== "modules" && - activityItem.field !== "attachment" && - activityItem.field !== "link" && - activityItem.field !== "estimate" && - !activityItem.field ? ( - - created - - ) : ( - - ); - - if ("field" in activityItem && activityItem.field !== "updated_by") { - return ( -
  • -
    -
    - <> -
    -
    -
    -
    - {activityItem.field ? ( - activityItem.new_value === "restore" ? ( - - ) : ( - - ) - ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.display_name} - ) : ( -
    - {activityItem.actor_detail.display_name?.charAt(0)} -
    - )} -
    -
    -
    -
    -
    -
    - {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( - Plane - ) : activityItem.actor_detail.is_bot ? ( - - {activityItem.actor_detail.first_name} Bot - - ) : ( - - - {currentUser?.id === activityItem.actor_detail.id - ? "You" - : activityItem.actor_detail.display_name} - - - )}{" "} -
    - {message}{" "} - - {calculateTimeAgo(activityItem.created_at)} - -
    -
    -
    - -
    -
    -
  • - ); - } - })} -
-
- ) : ( - - )} +
+ {activityPages} + {pageCount < totalPages && resultsCount !== 0 && ( +
+ +
+ )} +
); diff --git a/web/pages/profile/change-password.tsx b/web/pages/profile/change-password.tsx index 80e2965d6..7e9344753 100644 --- a/web/pages/profile/change-password.tsx +++ b/web/pages/profile/change-password.tsx @@ -1,22 +1,20 @@ import { ReactElement, useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // hooks +import { Button, Input, Spinner, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; +import { PageHead } from "components/core"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { useApplication, useUser } from "hooks/store"; // services -import { UserService } from "services/user.service"; // components -import { PageHead } from "components/core"; -// hooks -import useToast from "hooks/use-toast"; // layout import { ProfileSettingsLayout } from "layouts/settings-layout"; // ui -import { Button, Input, Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; -import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { UserService } from "services/user.service"; interface FormValues { old_password: string; @@ -46,33 +44,28 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => { handleSubmit, formState: { errors, isSubmitting }, } = useForm({ defaultValues }); - const { setToastAlert } = useToast(); const handleChangePassword = async (formData: FormValues) => { if (formData.new_password !== formData.confirm_password) { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "The new password and the confirm password don't match.", }); return; } - await userService - .changePassword(formData) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Password changed successfully.", - }); - }) - .catch((error) => { - setToastAlert({ - type: "error", - title: "Error!", - message: error?.error ?? "Something went wrong. Please try again.", - }); - }); + const changePasswordPromise = userService.changePassword(formData); + setPromiseToast(changePasswordPromise, { + loading: "Changing password...", + success: { + title: "Success!", + message: () => "Password changed successfully.", + }, + error: { + title: "Error!", + message: () => "Something went wrong. Please try again.", + }, + }); }; useEffect(() => { @@ -92,8 +85,8 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => { return ( <> -
-
+
+
themeStore.toggleSidebar()} />
= { avatar: "", @@ -52,8 +65,6 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { control, formState: { errors }, } = useForm({ defaultValues }); - // toast alert - const { setToastAlert } = useToast(); // store hooks const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser(); // custom hooks @@ -72,28 +83,26 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { avatar: formData.avatar, cover_image: formData.cover_image, role: formData.role, - display_name: formData.display_name, + display_name: formData?.display_name, user_timezone: formData.user_timezone, }; - await updateCurrentUser(payload) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Profile updated successfully.", - }); - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "There was some error in updating your profile. Please try again.", - }) - ); - setTimeout(() => { - setIsLoading(false); - }, 300); + const updateCurrentUserDetail = updateCurrentUser(payload).finally(() => setIsLoading(false)); + setPromiseToast(updateCurrentUserDetail, { + loading: "Updating...", + success: { + title: "Success!", + message: () => `Profile updated successfully.`, + }, + error: { + title: "Error!", + message: () => `There was some error in updating your profile. Please try again.`, + }, + }); + + // setTimeout(() => { + // setIsLoading(false); + // }, 300); }; const handleDelete = (url: string | null | undefined, updateUser: boolean = false) => { @@ -105,16 +114,16 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { if (updateUser) updateCurrentUser({ avatar: "" }) .then(() => { - setToastAlert({ - type: "success", + setToast({ + type: TOAST_TYPE.SUCCESS, title: "Success!", - message: "Profile picture removed successfully.", + message: "Profile picture deleted successfully.", }); setIsRemoving(false); }) .catch(() => { - setToastAlert({ - type: "error", + setToast({ + type: TOAST_TYPE.ERROR, title: "Error!", message: "There was some error in deleting your profile picture. Please try again.", }); @@ -139,8 +148,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { return ( <> -
-
+
+
themeStore.toggleSidebar()} />
@@ -163,7 +172,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { )} /> setDeactivateAccountModal(false)} /> -
+
@@ -186,7 +195,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { src={watch("avatar")} className="absolute left-0 top-0 h-full w-full rounded-lg object-cover" onClick={() => setIsImageUploadModalOpen(true)} - alt={myProfile.display_name} + alt={myProfile?.display_name} role="button" />
@@ -299,7 +308,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { ref={ref} hasError={Boolean(errors.email)} placeholder="Enter your email" - className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${ + className={`w-full cursor-not-allowed rounded-md !bg-custom-background-80 ${ errors.email ? "border-red-500" : "" }`} disabled @@ -368,14 +377,14 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { value={value} onChange={onChange} ref={ref} - hasError={Boolean(errors.display_name)} + hasError={Boolean(errors?.display_name)} placeholder="Enter your display name" - className={`w-full ${errors.display_name ? "border-red-500" : ""}`} + className={`w-full ${errors?.display_name ? "border-red-500" : ""}`} maxLength={24} /> )} /> - {errors.display_name && Please enter display name} + {errors?.display_name && Please enter display name}
diff --git a/web/pages/profile/preferences/email.tsx b/web/pages/profile/preferences/email.tsx index 34bd6fb03..b34a493e5 100644 --- a/web/pages/profile/preferences/email.tsx +++ b/web/pages/profile/preferences/email.tsx @@ -1,16 +1,16 @@ import { ReactElement } from "react"; import useSWR from "swr"; // layouts +import { PageHead } from "components/core"; +import { EmailNotificationForm } from "components/profile/preferences"; +import { EmailSettingsLoader } from "components/ui"; import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; // ui -import { EmailSettingsLoader } from "components/ui"; // components -import { EmailNotificationForm } from "components/profile/preferences"; -import { PageHead } from "components/core"; // services +import { NextPageWithLayout } from "lib/types"; import { UserService } from "services/user.service"; // type -import { NextPageWithLayout } from "lib/types"; // services const userService = new UserService(); @@ -28,7 +28,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = () => { return ( <> -
+
diff --git a/web/pages/profile/preferences/theme.tsx b/web/pages/profile/preferences/theme.tsx index 134ace79e..e23e94c66 100644 --- a/web/pages/profile/preferences/theme.tsx +++ b/web/pages/profile/preferences/theme.tsx @@ -1,17 +1,16 @@ import { useEffect, useState, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useTheme } from "next-themes"; -// hooks -import { useUser } from "hooks/store"; -import useToast from "hooks/use-toast"; -// layouts -import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; +// ui +import { Spinner, setPromiseToast } from "@plane/ui"; // components import { CustomThemeSelector, ThemeSwitch, PageHead } from "components/core"; -// ui -import { Spinner } from "@plane/ui"; // constants import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; +// hooks +import { useUser } from "hooks/store"; +// layouts +import { ProfilePreferenceSettingsLayout } from "layouts/settings-layout/profile/preferences"; // type import { NextPageWithLayout } from "lib/types"; @@ -24,7 +23,6 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { const userTheme = currentUser?.theme; // hooks const { setTheme } = useTheme(); - const { setToastAlert } = useToast(); useEffect(() => { if (userTheme) { @@ -37,11 +35,18 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { const handleThemeChange = (themeOption: I_THEME_OPTION) => { setTheme(themeOption.value); - updateCurrentUserTheme(themeOption.value).catch(() => { - setToastAlert({ - title: "Failed to Update the theme", - type: "error", - }); + const updateCurrentUserThemePromise = updateCurrentUserTheme(themeOption.value); + + setPromiseToast(updateCurrentUserThemePromise, { + loading: "Updating theme...", + success: { + title: "Success!", + message: () => "Theme updated successfully!", + }, + error: { + title: "Error!", + message: () => "Failed to Update the theme", + }, }); }; @@ -49,7 +54,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { <> {currentUser ? ( -
+

Preferences

diff --git a/web/pages/workspace-invitations/index.tsx b/web/pages/workspace-invitations/index.tsx index aa95d0a38..74c881125 100644 --- a/web/pages/workspace-invitations/index.tsx +++ b/web/pages/workspace-invitations/index.tsx @@ -1,22 +1,22 @@ import React, { ReactElement } from "react"; +import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Boxes, Check, Share2, Star, User2, X } from "lucide-react"; -import { observer } from "mobx-react-lite"; // hooks +import { Spinner } from "@plane/ui"; +import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space"; +import { WORKSPACE_INVITATION } from "constants/fetch-keys"; import { useUser } from "hooks/store"; // services -import { WorkspaceService } from "services/workspace.service"; // layouts import DefaultLayout from "layouts/default-layout"; // ui -import { Spinner } from "@plane/ui"; // icons -import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space"; // types import { NextPageWithLayout } from "lib/types"; +import { WorkspaceService } from "services/workspace.service"; // constants -import { WORKSPACE_INVITATION } from "constants/fetch-keys"; // services const workspaceService = new WorkspaceService(); diff --git a/web/public/empty-state/module-issues/gantt-dark-resp.webp b/web/public/empty-state/module-issues/gantt_chart-dark-resp.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-dark-resp.webp rename to web/public/empty-state/module-issues/gantt_chart-dark-resp.webp diff --git a/web/public/empty-state/module-issues/gantt-dark.webp b/web/public/empty-state/module-issues/gantt_chart-dark.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-dark.webp rename to web/public/empty-state/module-issues/gantt_chart-dark.webp diff --git a/web/public/empty-state/module-issues/gantt-light-resp.webp b/web/public/empty-state/module-issues/gantt_chart-light-resp.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-light-resp.webp rename to web/public/empty-state/module-issues/gantt_chart-light-resp.webp diff --git a/web/public/empty-state/module-issues/gantt-light.webp b/web/public/empty-state/module-issues/gantt_chart-light.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-light.webp rename to web/public/empty-state/module-issues/gantt_chart-light.webp diff --git a/web/services/ai.service.ts b/web/services/ai.service.ts index 11c489c1f..677f50e92 100644 --- a/web/services/ai.service.ts +++ b/web/services/ai.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { IGptResponse } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class AIService extends APIService { constructor() { diff --git a/web/services/analytics.service.ts b/web/services/analytics.service.ts index 5e3aac44b..972fe36ea 100644 --- a/web/services/analytics.service.ts +++ b/web/services/analytics.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { @@ -9,7 +10,6 @@ import { ISaveAnalyticsFormData, } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class AnalyticsService extends APIService { constructor() { diff --git a/web/services/api_token.service.ts b/web/services/api_token.service.ts index 76a24798f..3979f6e1f 100644 --- a/web/services/api_token.service.ts +++ b/web/services/api_token.service.ts @@ -1,6 +1,6 @@ import { API_BASE_URL } from "helpers/common.helper"; -import { APIService } from "./api.service"; import { IApiToken } from "@plane/types"; +import { APIService } from "./api.service"; export class APITokenService extends APIService { constructor() { diff --git a/web/services/app_config.service.ts b/web/services/app_config.service.ts index 4b45e0cc4..7c2d1e24e 100644 --- a/web/services/app_config.service.ts +++ b/web/services/app_config.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helper -import { API_BASE_URL } from "helpers/common.helper"; // types import { IAppConfig } from "@plane/types"; diff --git a/web/services/app_installation.service.ts b/web/services/app_installation.service.ts index 179721036..055a4b091 100644 --- a/web/services/app_installation.service.ts +++ b/web/services/app_installation.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class AppInstallationService extends APIService { constructor() { diff --git a/web/services/auth.service.ts b/web/services/auth.service.ts index f47a52824..f90fafc66 100644 --- a/web/services/auth.service.ts +++ b/web/services/auth.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IEmailCheckData, diff --git a/web/services/cycle.service.ts b/web/services/cycle.service.ts index 5e13e3b8e..f7ee8a0ab 100644 --- a/web/services/cycle.service.ts +++ b/web/services/cycle.service.ts @@ -1,9 +1,9 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { CycleDateCheckData, ICycle, TIssue } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class CycleService extends APIService { constructor() { diff --git a/web/services/dashboard.service.ts b/web/services/dashboard.service.ts index e001f92a1..b1138899d 100644 --- a/web/services/dashboard.service.ts +++ b/web/services/dashboard.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { THomeDashboardResponse, TWidget, TWidgetStatsResponse, TWidgetStatsRequestParams } from "@plane/types"; diff --git a/web/services/file.service.ts b/web/services/file.service.ts index d5e80dd53..0818bc992 100644 --- a/web/services/file.service.ts +++ b/web/services/file.service.ts @@ -1,8 +1,8 @@ // services +import axios from "axios"; +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; -import axios from "axios"; export interface UnSplashImage { id: string; diff --git a/web/services/inbox.service.ts b/web/services/inbox.service.ts index a36d356ce..45f0172fb 100644 --- a/web/services/inbox.service.ts +++ b/web/services/inbox.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { IInboxIssue, IInbox, TInboxStatus, IInboxQueryParams } from "@plane/types"; diff --git a/web/services/inbox/inbox-issue.service.ts b/web/services/inbox/inbox-issue.service.ts index 6b2099059..e6d52768c 100644 --- a/web/services/inbox/inbox-issue.service.ts +++ b/web/services/inbox/inbox-issue.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { TInboxIssueFilterOptions, TInboxIssueExtendedDetail, TIssue, TInboxDetailedStatus } from "@plane/types"; diff --git a/web/services/inbox/inbox.service.ts b/web/services/inbox/inbox.service.ts index 8ee6ee514..fc5fa5a99 100644 --- a/web/services/inbox/inbox.service.ts +++ b/web/services/inbox/inbox.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { TInbox } from "@plane/types"; diff --git a/web/services/instance.service.ts b/web/services/instance.service.ts index 1bc5ecdbc..f61370a91 100644 --- a/web/services/instance.service.ts +++ b/web/services/instance.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types"; diff --git a/web/services/integrations/github.service.ts b/web/services/integrations/github.service.ts index 6a0519565..5c4c95c09 100644 --- a/web/services/integrations/github.service.ts +++ b/web/services/integrations/github.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IGithubRepoInfo, IGithubServiceImportFormData } from "@plane/types"; diff --git a/web/services/integrations/integration.service.ts b/web/services/integrations/integration.service.ts index 460dc17d9..a1bb10078 100644 --- a/web/services/integrations/integration.service.ts +++ b/web/services/integrations/integration.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { IAppIntegration, IImporterService, IWorkspaceIntegration, IExportServiceResponse } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IntegrationService extends APIService { constructor() { diff --git a/web/services/integrations/jira.service.ts b/web/services/integrations/jira.service.ts index 5641bb28b..8c254bbab 100644 --- a/web/services/integrations/jira.service.ts +++ b/web/services/integrations/jira.service.ts @@ -1,5 +1,5 @@ -import { APIService } from "services/api.service"; import { API_BASE_URL } from "helpers/common.helper"; +import { APIService } from "services/api.service"; // types import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "@plane/types"; diff --git a/web/services/issue/issue.service.ts b/web/services/issue/issue.service.ts index 316288278..d7f92f792 100644 --- a/web/services/issue/issue.service.ts +++ b/web/services/issue/issue.service.ts @@ -1,9 +1,9 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // type import type { TIssue, IIssueDisplayProperties, TIssueLink, TIssueSubIssues, TIssueActivity } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IssueService extends APIService { constructor() { diff --git a/web/services/issue/issue_activity.service.ts b/web/services/issue/issue_activity.service.ts index 87c7a8f54..9028568ad 100644 --- a/web/services/issue/issue_activity.service.ts +++ b/web/services/issue/issue_activity.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { TIssueActivity } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IssueActivityService extends APIService { constructor() { diff --git a/web/services/issue/issue_archive.service.ts b/web/services/issue/issue_archive.service.ts index e2a5132a5..e232e796f 100644 --- a/web/services/issue/issue_archive.service.ts +++ b/web/services/issue/issue_archive.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { TIssue } from "@plane/types"; // constants -import { API_BASE_URL } from "helpers/common.helper"; export class IssueArchiveService extends APIService { constructor() { diff --git a/web/services/issue/issue_attachment.service.ts b/web/services/issue/issue_attachment.service.ts index 16253218a..00673c963 100644 --- a/web/services/issue/issue_attachment.service.ts +++ b/web/services/issue/issue_attachment.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helper -import { API_BASE_URL } from "helpers/common.helper"; // types import { TIssueAttachment } from "@plane/types"; diff --git a/web/services/issue/issue_comment.service.ts b/web/services/issue/issue_comment.service.ts index 8001d644a..d7ef35df7 100644 --- a/web/services/issue/issue_comment.service.ts +++ b/web/services/issue/issue_comment.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { TIssueComment } from "@plane/types"; // helper -import { API_BASE_URL } from "helpers/common.helper"; export class IssueCommentService extends APIService { constructor() { diff --git a/web/services/issue/issue_draft.service.ts b/web/services/issue/issue_draft.service.ts index a93bda776..3ccd43f56 100644 --- a/web/services/issue/issue_draft.service.ts +++ b/web/services/issue/issue_draft.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; import { TIssue } from "@plane/types"; export class IssueDraftService extends APIService { diff --git a/web/services/issue_filter.service.ts b/web/services/issue_filter.service.ts index 5103a4bc8..664666a3b 100644 --- a/web/services/issue_filter.service.ts +++ b/web/services/issue_filter.service.ts @@ -1,8 +1,8 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { IIssueFiltersResponse } from "@plane/types"; -import { API_BASE_URL } from "helpers/common.helper"; export class IssueFiltersService extends APIService { constructor() { diff --git a/web/services/module.service.ts b/web/services/module.service.ts index 1efad8a23..9942f691c 100644 --- a/web/services/module.service.ts +++ b/web/services/module.service.ts @@ -1,8 +1,8 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { IModule, TIssue, ILinkDetails, ModuleLink } from "@plane/types"; -import { API_BASE_URL } from "helpers/common.helper"; export class ModuleService extends APIService { constructor() { diff --git a/web/services/notification.service.ts b/web/services/notification.service.ts index db9c6d6d1..d12656c09 100644 --- a/web/services/notification.service.ts +++ b/web/services/notification.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { @@ -9,7 +10,6 @@ import type { IMarkAllAsReadPayload, } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class NotificationService extends APIService { constructor() { diff --git a/web/services/project/project-estimate.service.ts b/web/services/project/project-estimate.service.ts index 6d276c7b9..880c4dd8d 100644 --- a/web/services/project/project-estimate.service.ts +++ b/web/services/project/project-estimate.service.ts @@ -1,9 +1,9 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { IEstimate, IEstimateFormData, IEstimatePoint } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class ProjectEstimateService extends APIService { constructor() { diff --git a/web/services/project/project-export.service.ts b/web/services/project/project-export.service.ts index b5503a829..cc8cebe71 100644 --- a/web/services/project/project-export.service.ts +++ b/web/services/project/project-export.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class ProjectExportService extends APIService { constructor() { diff --git a/web/services/project/project-state.service.ts b/web/services/project/project-state.service.ts index 9f846987e..4087ada30 100644 --- a/web/services/project/project-state.service.ts +++ b/web/services/project/project-state.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import type { IState } from "@plane/types"; diff --git a/web/services/user.service.ts b/web/services/user.service.ts index 13ffa9c51..691e6c028 100644 --- a/web/services/user.service.ts +++ b/web/services/user.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import type { @@ -9,11 +10,9 @@ import type { IUserProfileData, IUserProfileProjectSegregation, IUserSettings, - IUserWorkspaceDashboard, IUserEmailNotificationSettings, } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class UserService extends APIService { constructor() { @@ -113,20 +112,8 @@ export class UserService extends APIService { }); } - async getUserActivity(): Promise { - return this.get(`/api/users/me/activities/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async userWorkspaceDashboard(workspaceSlug: string, month: number): Promise { - return this.get(`/api/users/me/workspaces/${workspaceSlug}/dashboard/`, { - params: { - month: month, - }, - }) + async getUserActivity(params: { per_page: number; cursor?: string }): Promise { + return this.get("/api/users/me/activities/", { params }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -160,8 +147,31 @@ export class UserService extends APIService { }); } - async getUserProfileActivity(workspaceSlug: string, userId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/?per_page=15`) + async getUserProfileActivity( + workspaceSlug: string, + userId: string, + params: { + per_page: number; + cursor?: string; + } + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async downloadProfileActivity( + workspaceSlug: string, + userId: string, + data: { + date: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/export/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/services/view.service.ts b/web/services/view.service.ts index 95ae7dd06..f09eea563 100644 --- a/web/services/view.service.ts +++ b/web/services/view.service.ts @@ -1,8 +1,8 @@ +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // types import { IProjectView } from "@plane/types"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; export class ViewService extends APIService { constructor() { diff --git a/web/services/webhook.service.ts b/web/services/webhook.service.ts index abfe7c46d..d021799fb 100644 --- a/web/services/webhook.service.ts +++ b/web/services/webhook.service.ts @@ -1,7 +1,7 @@ // api services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IWebhook } from "@plane/types"; diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index 2515853f5..bfeadad03 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -1,7 +1,7 @@ // services +import { API_BASE_URL } from "helpers/common.helper"; import { APIService } from "services/api.service"; // helpers -import { API_BASE_URL } from "helpers/common.helper"; // types import { IWorkspace, diff --git a/web/store/application/app-config.store.ts b/web/store/application/app-config.store.ts index 6faef8b69..aec22a4ce 100644 --- a/web/store/application/app-config.store.ts +++ b/web/store/application/app-config.store.ts @@ -1,8 +1,8 @@ import { observable, action, makeObservable, runInAction } from "mobx"; // types +import { AppConfigService } from "services/app_config.service"; import { IAppConfig } from "@plane/types"; // services -import { AppConfigService } from "services/app_config.service"; export interface IAppConfigStore { // observables diff --git a/web/store/application/command-palette.store.ts b/web/store/application/command-palette.store.ts index 22b395e34..dc10bba88 100644 --- a/web/store/application/command-palette.store.ts +++ b/web/store/application/command-palette.store.ts @@ -1,8 +1,8 @@ import { observable, action, makeObservable, computed } from "mobx"; // services -import { ProjectService } from "services/project"; -import { PageService } from "services/page.service"; import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue"; +import { PageService } from "services/page.service"; +import { ProjectService } from "services/project"; export interface ModalData { store: EIssuesStoreType; diff --git a/web/store/application/index.ts b/web/store/application/index.ts index 30333535a..bad28d4c9 100644 --- a/web/store/application/index.ts +++ b/web/store/application/index.ts @@ -1,7 +1,7 @@ import { RootStore } from "store/root.store"; +import { EventTrackerStore, IEventTrackerStore } from "../event-tracker.store"; import { AppConfigStore, IAppConfigStore } from "./app-config.store"; import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.store"; -import { EventTrackerStore, IEventTrackerStore } from "../event-tracker.store"; // import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; import { InstanceStore, IInstanceStore } from "./instance.store"; import { RouterStore, IRouterStore } from "./router.store"; diff --git a/web/store/application/instance.store.ts b/web/store/application/instance.store.ts index 7c486ef8b..b4793fdfb 100644 --- a/web/store/application/instance.store.ts +++ b/web/store/application/instance.store.ts @@ -1,8 +1,8 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"; // types +import { InstanceService } from "services/instance.service"; import { IInstance, IInstanceConfiguration, IFormattedInstanceConfiguration, IInstanceAdmin } from "@plane/types"; // services -import { InstanceService } from "services/instance.service"; export interface IInstanceStore { // issues diff --git a/web/store/application/theme.store.ts b/web/store/application/theme.store.ts index 3873e7386..0f40e86ae 100644 --- a/web/store/application/theme.store.ts +++ b/web/store/application/theme.store.ts @@ -6,14 +6,12 @@ import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; export interface IThemeStore { // observables theme: string | null; - mobileSidebarCollapsed: boolean | undefined; sidebarCollapsed: boolean | undefined; profileSidebarCollapsed: boolean | undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined; issueDetailSidebarCollapsed: boolean | undefined; // actions toggleSidebar: (collapsed?: boolean) => void; - toggleMobileSidebar: (collapsed?: boolean) => void; setTheme: (theme: any) => void; toggleProfileSidebar: (collapsed?: boolean) => void; toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void; @@ -22,7 +20,6 @@ export interface IThemeStore { export class ThemeStore implements IThemeStore { // observables - mobileSidebarCollapsed: boolean | undefined = true; sidebarCollapsed: boolean | undefined = undefined; theme: string | null = null; profileSidebarCollapsed: boolean | undefined = undefined; @@ -34,7 +31,6 @@ export class ThemeStore implements IThemeStore { constructor(_rootStore: any | null = null) { makeObservable(this, { // observable - mobileSidebarCollapsed: observable.ref, sidebarCollapsed: observable.ref, theme: observable.ref, profileSidebarCollapsed: observable.ref, @@ -42,7 +38,6 @@ export class ThemeStore implements IThemeStore { issueDetailSidebarCollapsed: observable.ref, // action toggleSidebar: action, - toggleMobileSidebar: action, setTheme: action, toggleProfileSidebar: action, toggleWorkspaceAnalyticsSidebar: action, @@ -66,19 +61,6 @@ export class ThemeStore implements IThemeStore { localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString()); }; - /** - * Toggle mobile sidebar collapsed state - * @param collapsed - */ - toggleMobileSidebar = (collapsed?: boolean) => { - if (collapsed === undefined) { - this.mobileSidebarCollapsed = !this.mobileSidebarCollapsed; - } else { - this.mobileSidebarCollapsed = collapsed; - } - localStorage.setItem("mobile_sidebar_collapsed", this.mobileSidebarCollapsed.toString()); - }; - /** * Toggle the profile sidebar collapsed state * @param collapsed diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index ee4842539..aea87033e 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -1,16 +1,16 @@ -import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import { isFuture, isPast, isToday } from "date-fns"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // types -import { ICycle, CycleDateCheckData } from "@plane/types"; // mobx -import { RootStore } from "store/root.store"; // services -import { ProjectService } from "services/project"; -import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; +import { IssueService } from "services/issue"; +import { ProjectService } from "services/project"; +import { RootStore } from "store/root.store"; +import { ICycle, CycleDateCheckData } from "@plane/types"; export interface ICycleStore { //Loaders diff --git a/web/store/dashboard.store.ts b/web/store/dashboard.store.ts index ad0960c7b..c8a07428e 100644 --- a/web/store/dashboard.store.ts +++ b/web/store/dashboard.store.ts @@ -1,6 +1,6 @@ +import set from "lodash/set"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import set from "lodash/set"; // services import { DashboardService } from "services/dashboard.service"; // types diff --git a/web/store/estimate.store.ts b/web/store/estimate.store.ts index beddd52ab..9c197ffaa 100644 --- a/web/store/estimate.store.ts +++ b/web/store/estimate.store.ts @@ -1,11 +1,11 @@ -import { observable, action, makeObservable, runInAction, computed } from "mobx"; import set from "lodash/set"; +import { observable, action, makeObservable, runInAction, computed } from "mobx"; // services +import { computedFn } from "mobx-utils"; import { ProjectEstimateService } from "services/project"; // types import { RootStore } from "store/root.store"; import { IEstimate, IEstimateFormData } from "@plane/types"; -import { computedFn } from "mobx-utils"; export interface IEstimateStore { //Loaders diff --git a/web/store/event-tracker.store.ts b/web/store/event-tracker.store.ts index 744ad44fb..f117e6cbc 100644 --- a/web/store/event-tracker.store.ts +++ b/web/store/event-tracker.store.ts @@ -1,7 +1,6 @@ import { action, computed, makeObservable, observable } from "mobx"; import posthog from "posthog-js"; // stores -import { RootStore } from "./root.store"; import { GROUP_WORKSPACE, WORKSPACE_CREATED, @@ -15,6 +14,7 @@ import { getWorkspaceEventPayload, getPageEventPayload, } from "constants/event-tracker"; +import { RootStore } from "./root.store"; export interface IEventTrackerStore { // properties diff --git a/web/store/global-view.store.ts b/web/store/global-view.store.ts index 65aedadb5..60d97f633 100644 --- a/web/store/global-view.store.ts +++ b/web/store/global-view.store.ts @@ -1,6 +1,6 @@ +import { set } from "lodash"; import { observable, action, makeObservable, runInAction, computed } from "mobx"; import { computedFn } from "mobx-utils"; -import { set } from "lodash"; // services import { WorkspaceService } from "services/workspace.service"; // types diff --git a/web/store/inbox/inbox.store.ts b/web/store/inbox/inbox.store.ts index 8d8f2bec5..803af3095 100644 --- a/web/store/inbox/inbox.store.ts +++ b/web/store/inbox/inbox.store.ts @@ -1,9 +1,9 @@ +import concat from "lodash/concat"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; import { observable, action, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import set from "lodash/set"; -import update from "lodash/update"; -import concat from "lodash/concat"; -import uniq from "lodash/uniq"; // services import { InboxService } from "services/inbox/inbox.service"; // types diff --git a/web/store/inbox/inbox_filter.store.ts b/web/store/inbox/inbox_filter.store.ts index c4566acbe..8bad22cdd 100644 --- a/web/store/inbox/inbox_filter.store.ts +++ b/web/store/inbox/inbox_filter.store.ts @@ -1,6 +1,6 @@ -import { observable, action, makeObservable, runInAction, computed } from "mobx"; -import set from "lodash/set"; import isEmpty from "lodash/isEmpty"; +import set from "lodash/set"; +import { observable, action, makeObservable, runInAction, computed } from "mobx"; // services import { InboxService } from "services/inbox.service"; // types diff --git a/web/store/inbox/inbox_issue.store.ts b/web/store/inbox/inbox_issue.store.ts index 4f980357f..2ecbedff0 100644 --- a/web/store/inbox/inbox_issue.store.ts +++ b/web/store/inbox/inbox_issue.store.ts @@ -1,10 +1,10 @@ +import concat from "lodash/concat"; +import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; import { observable, action, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import set from "lodash/set"; -import update from "lodash/update"; -import concat from "lodash/concat"; -import uniq from "lodash/uniq"; -import pull from "lodash/pull"; // services import { InboxIssueService } from "services/inbox/inbox-issue.service"; // types diff --git a/web/store/inbox/root.store.ts b/web/store/inbox/root.store.ts index b0706cca7..982de47bc 100644 --- a/web/store/inbox/root.store.ts +++ b/web/store/inbox/root.store.ts @@ -1,8 +1,8 @@ // types import { RootStore } from "store/root.store"; import { IInbox, Inbox } from "./inbox.store"; -import { IInboxIssue, InboxIssue } from "./inbox_issue.store"; import { IInboxFilter, InboxFilter } from "./inbox_filter.store"; +import { IInboxIssue, InboxIssue } from "./inbox_issue.store"; export interface IInboxRootStore { rootStore: RootStore; diff --git a/web/store/issue/archived/filter.store.ts b/web/store/issue/archived/filter.store.ts index 032928cda..12d541bea 100644 --- a/web/store/issue/archived/filter.store.ts +++ b/web/store/issue/archived/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IArchivedIssuesFilter { // observables diff --git a/web/store/issue/archived/issue.store.ts b/web/store/issue/archived/issue.store.ts index a0b26eb8b..14a7b4008 100644 --- a/web/store/issue/archived/issue.store.ts +++ b/web/store/issue/archived/issue.store.ts @@ -1,13 +1,13 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { IssueArchiveService } from "services/issue"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IArchivedIssues { // observable @@ -17,7 +17,7 @@ export interface IArchivedIssues { // computed groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; // actions - fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; + fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; quickAddIssue: undefined; diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index b938a36d4..6d5050b59 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface ICycleIssuesFilter { // observables @@ -35,7 +35,7 @@ export interface ICycleIssuesFilter { projectId: string, filterType: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - cycleId?: string | undefined + cycleId: string ) => Promise; } @@ -84,6 +84,8 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); if (!filteredParams) return undefined; + if (filteredParams.includes("cycle")) filteredParams.splice(filteredParams.indexOf("cycle"), 1); + const filteredRouteParams: Partial> = this.computedFilteredParams( userFilters?.filters as IIssueFilterOptions, userFilters?.displayFilters as IIssueDisplayFilterOptions, @@ -134,10 +136,9 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI projectId: string, type: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - cycleId: string | undefined = undefined + cycleId: string ) => { try { - if (!cycleId) throw new Error("Cycle id is required"); if (isEmpty(this.filters) || isEmpty(this.filters[cycleId]) || isEmpty(filters)) return; const _filters = { diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 61b280da9..b270f0054 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -1,17 +1,17 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import set from "lodash/set"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class -import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; +import { IssueService } from "services/issue"; // types -import { IIssueRootStore } from "../root.store"; import { TIssue, TSubGroupedIssues, TGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { IssueHelperStore } from "../helpers/issue-helper.store"; +import { IIssueRootStore } from "../root.store"; export const ACTIVE_CYCLE_ISSUES = "ACTIVE_CYCLE_ISSUES"; @@ -27,33 +27,23 @@ export interface ICycleIssues { workspaceSlug: string, projectId: string, loadType: TLoader, - cycleId?: string | undefined + cycleId: string ) => Promise; createIssue: ( workspaceSlug: string, projectId: string, data: Partial, - cycleId?: string | undefined + cycleId: string ) => Promise; updateIssue: ( workspaceSlug: string, projectId: string, issueId: string, data: Partial, - cycleId?: string | undefined - ) => Promise; - removeIssue: ( - workspaceSlug: string, - projectId: string, - issueId: string, - cycleId?: string | undefined - ) => Promise; - archiveIssue: ( - workspaceSlug: string, - projectId: string, - issueId: string, - cycleId?: string | undefined + cycleId: string ) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string, cycleId: string) => Promise; + archiveIssue: (workspaceSlug: string, projectId: string, issueId: string, cycleId: string) => Promise; quickAddIssue: ( workspaceSlug: string, projectId: string, @@ -67,7 +57,7 @@ export interface ICycleIssues { issueIds: string[], fetchAddedIssues?: boolean ) => Promise; - removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; + removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; transferIssuesFromCycle: ( workspaceSlug: string, projectId: string, @@ -156,11 +146,9 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader", - cycleId: string | undefined = undefined + cycleId: string ) => { try { - if (!cycleId) throw new Error("Cycle Id is required"); - this.loader = loadType; const params = this.rootIssueStore?.cycleIssuesFilter?.appliedFilters; @@ -185,15 +173,8 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { } }; - createIssue = async ( - workspaceSlug: string, - projectId: string, - data: Partial, - cycleId: string | undefined = undefined - ) => { + createIssue = async (workspaceSlug: string, projectId: string, data: Partial, cycleId: string) => { try { - if (!cycleId) throw new Error("Cycle Id is required"); - const response = await this.rootIssueStore.projectIssues.createIssue(workspaceSlug, projectId, data); await this.addIssueToCycle(workspaceSlug, projectId, cycleId, [response.id], false); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); @@ -209,11 +190,9 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { projectId: string, issueId: string, data: Partial, - cycleId: string | undefined = undefined + cycleId: string ) => { try { - if (!cycleId) throw new Error("Cycle Id is required"); - await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); } catch (error) { @@ -222,15 +201,8 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { } }; - removeIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - cycleId: string | undefined = undefined - ) => { + removeIssue = async (workspaceSlug: string, projectId: string, issueId: string, cycleId: string) => { try { - if (!cycleId) throw new Error("Cycle Id is required"); - await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); @@ -244,15 +216,8 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { } }; - archiveIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - cycleId: string | undefined = undefined - ) => { + archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string, cycleId: string) => { try { - if (!cycleId) throw new Error("Cycle Id is required"); - await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); @@ -290,7 +255,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { return response; } catch (error) { - this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); + if (cycleId) this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); throw error; } }; @@ -335,10 +300,8 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { this.rootStore.issues.updateIssue(issueId, { cycle_id: null }); - const response = await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); + await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); - - return response; } catch (error) { throw error; } diff --git a/web/store/issue/draft/filter.store.ts b/web/store/issue/draft/filter.store.ts index cc58a7755..51b8d9bc7 100644 --- a/web/store/issue/draft/filter.store.ts +++ b/web/store/issue/draft/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IDraftIssuesFilter { // observables diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index a06213eb0..67dcf2729 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -1,16 +1,16 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; -import uniq from "lodash/uniq"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class -import { IssueHelperStore } from "../helpers/issue-helper.store"; // services import { IssueDraftService } from "services/issue/issue_draft.service"; // types -import { IIssueRootStore } from "../root.store"; import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { IssueHelperStore } from "../helpers/issue-helper.store"; +import { IIssueRootStore } from "../root.store"; export interface IDraftIssues { // observable diff --git a/web/store/issue/helpers/issue-filter-helper.store.ts b/web/store/issue/helpers/issue-filter-helper.store.ts index 8ff45ed09..2921e9ca8 100644 --- a/web/store/issue/helpers/issue-filter-helper.store.ts +++ b/web/store/issue/helpers/issue-filter-helper.store.ts @@ -1,5 +1,9 @@ import isEmpty from "lodash/isEmpty"; // types +// constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +// lib +import { storage } from "lib/local-storage"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, @@ -10,10 +14,6 @@ import { TIssueParams, TStaticViewTypes, } from "@plane/types"; -// constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -// lib -import { storage } from "lib/local-storage"; interface ILocalStoreIssueFilters { key: EIssuesStoreType; @@ -74,6 +74,8 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { mentions: filters?.mentions || undefined, created_by: filters?.created_by || undefined, labels: filters?.labels || undefined, + cycle: filters?.cycle || undefined, + module: filters?.module || undefined, start_date: filters?.start_date || undefined, target_date: filters?.target_date || undefined, project: filters.project || undefined, @@ -107,6 +109,8 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { mentions: filters?.mentions || null, created_by: filters?.created_by || null, labels: filters?.labels || null, + cycle: filters?.cycle || null, + module: filters?.module || null, start_date: filters?.start_date || null, target_date: filters?.target_date || null, project: filters?.project || null, diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index 9d498e900..50e04e890 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -1,16 +1,16 @@ -import orderBy from "lodash/orderBy"; import get from "lodash/get"; import indexOf from "lodash/indexOf"; import isEmpty from "lodash/isEmpty"; +import orderBy from "lodash/orderBy"; import values from "lodash/values"; // types -import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types"; -import { IIssueRootStore } from "../root.store"; // constants import { ISSUE_PRIORITIES } from "constants/issue"; import { STATE_GROUPS } from "constants/state"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types"; +import { IIssueRootStore } from "../root.store"; export type TIssueDisplayFilterOptions = Exclude | "target_date"; @@ -36,6 +36,8 @@ export type TIssueHelperStore = { const ISSUE_FILTER_DEFAULT_DATA: Record = { project: "project_id", + cycle: "cycle_id", + module: "module_ids", state: "state_id", "state_detail.group": "state_group" as keyof TIssue, // state_detail.group is only being used for state_group display, priority: "priority", @@ -74,8 +76,8 @@ export class IssueHelperStore implements TIssueHelperStore { let groupArray = []; if (groupBy === "state_detail.group") { - const state_group = - this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None"; + // if groupBy state_detail.group is coming from the project level the we are using stateDetails from root store else we are looping through the stateMap + const state_group = (this.rootStore?.stateMap || {})?.[_issue?.state_id]?.group || "None"; groupArray = [state_group]; } else { const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); @@ -115,8 +117,8 @@ export class IssueHelperStore implements TIssueHelperStore { let subGroupArray = []; let groupArray = []; if (subGroupBy === "state_detail.group" || groupBy === "state_detail.group") { - const state_group = - this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None"; + const state_group = (this.rootStore?.stateMap || {})?.[_issue?.state_id]?.group || "None"; + subGroupArray = [state_group]; groupArray = [state_group]; } else { @@ -157,6 +159,10 @@ export class IssueHelperStore implements TIssueHelperStore { return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {}); case "project": return Object.keys(this.rootStore?.projectMap || {}); + case "cycle": + return Object.keys(this.rootStore?.cycleMap || {}); + case "module": + return Object.keys(this.rootStore?.moduleMap || {}); default: return []; } @@ -227,10 +233,10 @@ export class IssueHelperStore implements TIssueHelperStore { } /** - * This Method is mainly used to filter out empty values in the begining + * This Method is mainly used to filter out empty values in the beginning * @param key key of the value that is to be checked if empty * @param object any object in which the key's value is to be checked - * @returns 1 if emoty, 0 if not empty + * @returns 1 if empty, 0 if not empty */ getSortOrderToFilterEmptyValues(key: string, object: any) { const value = object?.[key]; diff --git a/web/store/issue/issue-details/activity.store.ts b/web/store/issue/issue-details/activity.store.ts index efa181c95..5afb6d8e4 100644 --- a/web/store/issue/issue-details/activity.store.ts +++ b/web/store/issue/issue-details/activity.store.ts @@ -1,14 +1,14 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; +import concat from "lodash/concat"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; -import update from "lodash/update"; -import concat from "lodash/concat"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { IssueActivityService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export type TActivityLoader = "fetch" | "mutate" | undefined; diff --git a/web/store/issue/issue-details/attachment.store.ts b/web/store/issue/issue-details/attachment.store.ts index 5341058c1..47e95b437 100644 --- a/web/store/issue/issue-details/attachment.store.ts +++ b/web/store/issue/issue-details/attachment.store.ts @@ -1,14 +1,14 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; -import uniq from "lodash/uniq"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services import { IssueAttachmentService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueAttachmentStoreActions { addAttachments: (issueId: string, attachments: TIssueAttachment[]) => void; diff --git a/web/store/issue/issue-details/comment.store.ts b/web/store/issue/issue-details/comment.store.ts index 4336971de..434be2778 100644 --- a/web/store/issue/issue-details/comment.store.ts +++ b/web/store/issue/issue-details/comment.store.ts @@ -1,14 +1,14 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; -import uniq from "lodash/uniq"; import pull from "lodash/pull"; +import set from "lodash/set"; +import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { IssueCommentService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueComment, TIssueCommentMap, TIssueCommentIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export type TCommentLoader = "fetch" | "create" | "update" | "delete" | "mutate" | undefined; diff --git a/web/store/issue/issue-details/comment_reaction.store.ts b/web/store/issue/issue-details/comment_reaction.store.ts index 59adeef62..832f798d9 100644 --- a/web/store/issue/issue-details/comment_reaction.store.ts +++ b/web/store/issue/issue-details/comment_reaction.store.ts @@ -1,16 +1,16 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import find from "lodash/find"; import pull from "lodash/pull"; +import set from "lodash/set"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services -import { IssueReactionService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; -import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types"; // helpers import { groupReactions } from "helpers/emoji.helper"; +import { IssueReactionService } from "services/issue"; +import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueCommentReactionStoreActions { // actions diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index f42c13376..92a9edf73 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -1,9 +1,9 @@ import { makeObservable } from "mobx"; // services +import { computedFn } from "mobx-utils"; import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue"; // types import { TIssue } from "@plane/types"; -import { computedFn } from "mobx-utils"; import { IIssueDetail } from "./root.store"; export interface IIssueStoreActions { @@ -18,7 +18,7 @@ export interface IIssueStoreActions { removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; - removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; + removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; removeModulesFromIssue: ( workspaceSlug: string, diff --git a/web/store/issue/issue-details/link.store.ts b/web/store/issue/issue-details/link.store.ts index 81d13438c..1cfd47c3f 100644 --- a/web/store/issue/issue-details/link.store.ts +++ b/web/store/issue/issue-details/link.store.ts @@ -1,10 +1,10 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services import { IssueService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueLink, TIssueLinkMap, TIssueLinkIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueLinkStoreActions { addLinks: (issueId: string, links: TIssueLink[]) => void; diff --git a/web/store/issue/issue-details/reaction.store.ts b/web/store/issue/issue-details/reaction.store.ts index 6282ac40e..a32ba6eca 100644 --- a/web/store/issue/issue-details/reaction.store.ts +++ b/web/store/issue/issue-details/reaction.store.ts @@ -1,16 +1,16 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import find from "lodash/find"; import pull from "lodash/pull"; +import set from "lodash/set"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services -import { IssueReactionService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; -import { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap } from "@plane/types"; // helpers import { groupReactions } from "helpers/emoji.helper"; +import { IssueReactionService } from "services/issue"; +import { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueReactionStoreActions { // actions diff --git a/web/store/issue/issue-details/relation.store.ts b/web/store/issue/issue-details/relation.store.ts index da729540e..fafa4ad4d 100644 --- a/web/store/issue/issue-details/relation.store.ts +++ b/web/store/issue/issue-details/relation.store.ts @@ -1,10 +1,10 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services import { IssueRelationService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssueRelationIdMap, TIssueRelationMap, TIssueRelationTypes, TIssueRelation, TIssue } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueRelationStoreActions { // actions diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index db5dab307..be77efcd1 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -1,20 +1,5 @@ import { action, computed, makeObservable, observable } from "mobx"; // types -import { IIssueRootStore } from "../root.store"; -import { IIssueStore, IssueStore, IIssueStoreActions } from "./issue.store"; -import { IIssueReactionStore, IssueReactionStore, IIssueReactionStoreActions } from "./reaction.store"; -import { IIssueLinkStore, IssueLinkStore, IIssueLinkStoreActions } from "./link.store"; -import { IIssueSubscriptionStore, IssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store"; -import { IIssueAttachmentStore, IssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store"; -import { IIssueSubIssuesStore, IssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.store"; -import { IIssueRelationStore, IssueRelationStore, IIssueRelationStoreActions } from "./relation.store"; -import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions, TActivityLoader } from "./activity.store"; -import { IIssueCommentStore, IssueCommentStore, IIssueCommentStoreActions, TCommentLoader } from "./comment.store"; -import { - IIssueCommentReactionStore, - IssueCommentReactionStore, - IIssueCommentReactionStoreActions, -} from "./comment_reaction.store"; import { TIssue, TIssueAttachment, @@ -24,6 +9,21 @@ import { TIssueReaction, TIssueRelationTypes, } from "@plane/types"; +import { IIssueRootStore } from "../root.store"; +import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions, TActivityLoader } from "./activity.store"; +import { IIssueAttachmentStore, IssueAttachmentStore, IIssueAttachmentStoreActions } from "./attachment.store"; +import { IIssueCommentStore, IssueCommentStore, IIssueCommentStoreActions, TCommentLoader } from "./comment.store"; +import { + IIssueCommentReactionStore, + IssueCommentReactionStore, + IIssueCommentReactionStoreActions, +} from "./comment_reaction.store"; +import { IIssueStore, IssueStore, IIssueStoreActions } from "./issue.store"; +import { IIssueLinkStore, IssueLinkStore, IIssueLinkStoreActions } from "./link.store"; +import { IIssueReactionStore, IssueReactionStore, IIssueReactionStoreActions } from "./reaction.store"; +import { IIssueRelationStore, IssueRelationStore, IIssueRelationStoreActions } from "./relation.store"; +import { IIssueSubIssuesStore, IssueSubIssuesStore, IIssueSubIssuesStoreActions } from "./sub_issues.store"; +import { IIssueSubscriptionStore, IssueSubscriptionStore, IIssueSubscriptionStoreActions } from "./subscription.store"; export type TPeekIssue = { workspaceSlug: string; diff --git a/web/store/issue/issue-details/sub_issues.store.ts b/web/store/issue/issue-details/sub_issues.store.ts index cfa1be12e..87ec58930 100644 --- a/web/store/issue/issue-details/sub_issues.store.ts +++ b/web/store/issue/issue-details/sub_issues.store.ts @@ -1,12 +1,11 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; import concat from "lodash/concat"; -import update from "lodash/update"; import pull from "lodash/pull"; +import set from "lodash/set"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { IssueService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssue, TIssueSubIssues, @@ -14,6 +13,7 @@ import { TIssueSubIssuesIdMap, TSubIssuesStateDistribution, } from "@plane/types"; +import { IIssueDetail } from "./root.store"; export interface IIssueSubIssuesStoreActions { fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise; diff --git a/web/store/issue/issue-details/subscription.store.ts b/web/store/issue/issue-details/subscription.store.ts index 276c952f4..48b353e72 100644 --- a/web/store/issue/issue-details/subscription.store.ts +++ b/web/store/issue/issue-details/subscription.store.ts @@ -1,5 +1,5 @@ -import { action, makeObservable, observable, runInAction } from "mobx"; import set from "lodash/set"; +import { action, makeObservable, observable, runInAction } from "mobx"; // services import { NotificationService } from "services/notification.service"; // types diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index cbda505ff..635c75b24 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -1,12 +1,12 @@ -import set from "lodash/set"; import isEmpty from "lodash/isEmpty"; +import set from "lodash/set"; // store import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types +import { IssueService } from "services/issue"; import { TIssue } from "@plane/types"; //services -import { IssueService } from "services/issue"; export type IIssueStore = { // observables diff --git a/web/store/issue/issue_calendar_view.store.ts b/web/store/issue/issue_calendar_view.store.ts index ac4a60809..98181d730 100644 --- a/web/store/issue/issue_calendar_view.store.ts +++ b/web/store/issue/issue_calendar_view.store.ts @@ -1,9 +1,9 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx"; // helpers +import { ICalendarPayload, ICalendarWeek } from "components/issues"; import { generateCalendarData } from "helpers/calendar.helper"; // types -import { ICalendarPayload, ICalendarWeek } from "components/issues"; import { getWeekNumberOfDate } from "helpers/date-time.helper"; export interface ICalendarStore { diff --git a/web/store/issue/issue_gantt_view.store.ts b/web/store/issue/issue_gantt_view.store.ts index b087554dd..e478e8649 100644 --- a/web/store/issue/issue_gantt_view.store.ts +++ b/web/store/issue/issue_gantt_view.store.ts @@ -1,9 +1,9 @@ import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // helpers +import { ChartDataType, TGanttViews } from "components/gantt-chart"; import { currentViewDataWithView } from "components/gantt-chart/data"; // types -import { ChartDataType, TGanttViews } from "components/gantt-chart"; export interface IGanttStore { // observables diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index f10a885a3..3f3dcb2ba 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IModuleIssuesFilter { // observables @@ -35,7 +35,7 @@ export interface IModuleIssuesFilter { projectId: string, filterType: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - moduleId?: string | undefined + moduleId: string ) => Promise; } @@ -84,6 +84,8 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); if (!filteredParams) return undefined; + if (filteredParams.includes("module")) filteredParams.splice(filteredParams.indexOf("module"), 1); + const filteredRouteParams: Partial> = this.computedFilteredParams( userFilters?.filters as IIssueFilterOptions, userFilters?.displayFilters as IIssueDisplayFilterOptions, @@ -134,10 +136,9 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul projectId: string, type: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - moduleId: string | undefined = undefined + moduleId: string ) => { try { - if (!moduleId) throw new Error("Module id is required"); if (isEmpty(this.filters) || isEmpty(this.filters[moduleId]) || isEmpty(filters)) return; const _filters = { diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index 9e6ad3f49..caa5331dc 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -1,17 +1,17 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; -import update from "lodash/update"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import set from "lodash/set"; import uniq from "lodash/uniq"; +import update from "lodash/update"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class -import { IssueHelperStore } from "../helpers/issue-helper.store"; // services import { IssueService } from "services/issue"; import { ModuleService } from "services/module.service"; // types -import { IIssueRootStore } from "../root.store"; import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; +import { IssueHelperStore } from "../helpers/issue-helper.store"; +import { IIssueRootStore } from "../root.store"; export interface IModuleIssues { // observable @@ -25,33 +25,23 @@ export interface IModuleIssues { workspaceSlug: string, projectId: string, loadType: TLoader, - moduleId?: string | undefined + moduleId: string ) => Promise; createIssue: ( workspaceSlug: string, projectId: string, data: Partial, - moduleId?: string | undefined + moduleId: string ) => Promise; updateIssue: ( workspaceSlug: string, projectId: string, issueId: string, data: Partial, - moduleId?: string | undefined - ) => Promise; - removeIssue: ( - workspaceSlug: string, - projectId: string, - issueId: string, - moduleId?: string | undefined - ) => Promise; - archiveIssue: ( - workspaceSlug: string, - projectId: string, - issueId: string, - moduleId?: string | undefined + moduleId: string ) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleId: string) => Promise; + archiveIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleId: string) => Promise; quickAddIssue: ( workspaceSlug: string, projectId: string, @@ -160,11 +150,9 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader", - moduleId: string | undefined = undefined + moduleId: string ) => { try { - if (!moduleId) throw new Error("Module Id is required"); - this.loader = loadType; const params = this.rootIssueStore?.moduleIssuesFilter?.appliedFilters; @@ -190,15 +178,8 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { } }; - createIssue = async ( - workspaceSlug: string, - projectId: string, - data: Partial, - moduleId: string | undefined = undefined - ) => { + createIssue = async (workspaceSlug: string, projectId: string, data: Partial, moduleId: string) => { try { - if (!moduleId) throw new Error("Module Id is required"); - const response = await this.rootIssueStore.projectIssues.createIssue(workspaceSlug, projectId, data); await this.addIssuesToModule(workspaceSlug, projectId, moduleId, [response.id], false); this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); @@ -214,11 +195,9 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { projectId: string, issueId: string, data: Partial, - moduleId: string | undefined = undefined + moduleId: string ) => { try { - if (!moduleId) throw new Error("Module Id is required"); - await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); } catch (error) { @@ -227,15 +206,8 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { } }; - removeIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - moduleId: string | undefined = undefined - ) => { + removeIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleId: string) => { try { - if (!moduleId) throw new Error("Module Id is required"); - await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); @@ -249,15 +221,8 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { } }; - archiveIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - moduleId: string | undefined = undefined - ) => { + archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string, moduleId: string) => { try { - if (!moduleId) throw new Error("Module Id is required"); - await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); diff --git a/web/store/issue/profile/filter.store.ts b/web/store/issue/profile/filter.store.ts index 658980082..f25f3d9b6 100644 --- a/web/store/issue/profile/filter.store.ts +++ b/web/store/issue/profile/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IProfileIssuesFilter { // observables @@ -36,7 +36,7 @@ export interface IProfileIssuesFilter { projectId: string | undefined, filterType: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - userId?: string | undefined + userId: string ) => Promise; } @@ -125,11 +125,9 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf projectId: string | undefined, type: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - userId: string | undefined = undefined + userId: string ) => { try { - if (!userId) throw new Error("user id is required"); - if (isEmpty(this.filters) || isEmpty(this.filters[userId]) || isEmpty(filters)) return; const _filters = { diff --git a/web/store/issue/profile/issue.store.ts b/web/store/issue/profile/issue.store.ts index c39b33a80..03e4e76f8 100644 --- a/web/store/issue/profile/issue.store.ts +++ b/web/store/issue/profile/issue.store.ts @@ -1,13 +1,13 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { UserService } from "services/user.service"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { UserService } from "services/user.service"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; interface IProfileIssueTabTypes { [key: string]: string[]; @@ -27,34 +27,24 @@ export interface IProfileIssues { workspaceSlug: string, projectId: string | undefined, loadType: TLoader, - userId?: string | undefined, + userId: string, view?: "assigned" | "created" | "subscribed" ) => Promise; createIssue: ( workspaceSlug: string, projectId: string, data: Partial, - userId?: string | undefined + userId: string ) => Promise; updateIssue: ( workspaceSlug: string, projectId: string, issueId: string, data: Partial, - userId?: string | undefined - ) => Promise; - removeIssue: ( - workspaceSlug: string, - projectId: string, - issueId: string, - userId?: string | undefined - ) => Promise; - archiveIssue: ( - workspaceSlug: string, - projectId: string, - issueId: string, - userId?: string | undefined + userId: string ) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string, userId: string) => Promise; + archiveIssue: (workspaceSlug: string, projectId: string, issueId: string, userId: string) => Promise; quickAddIssue: undefined; } @@ -150,15 +140,14 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { workspaceSlug: string, projectId: string | undefined, loadType: TLoader = "init-loader", - userId?: string | undefined, + userId: string, view?: "assigned" | "created" | "subscribed" ) => { try { - if (!userId) throw new Error("user id is required"); - if (!view) throw new Error("current tab view is required"); - this.loader = loadType; - this.currentView = view; + if (view) this.currentView = view; + + if (!this.currentView) throw new Error("current tab view is required"); const uniqueViewId = `${workspaceSlug}_${view}`; @@ -193,15 +182,8 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { } }; - createIssue = async ( - workspaceSlug: string, - projectId: string, - data: Partial, - userId: string | undefined = undefined - ) => { + createIssue = async (workspaceSlug: string, projectId: string, data: Partial, userId: string) => { try { - if (!userId) throw new Error("user id is required"); - const response = await this.rootIssueStore.projectIssues.createIssue(workspaceSlug, projectId, data); const uniqueViewId = `${workspaceSlug}_${this.currentView}`; @@ -223,11 +205,9 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { projectId: string, issueId: string, data: Partial, - userId: string | undefined = undefined + userId: string ) => { try { - if (!userId) throw new Error("user id is required"); - this.rootStore.issues.updateIssue(issueId, data); await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, data.id as keyof TIssue, data); } catch (error) { @@ -258,13 +238,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { } }; - archiveIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - userId: string | undefined = undefined - ) => { - if (!userId) return; + archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string, userId: string) => { try { await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); diff --git a/web/store/issue/project-views/filter.store.ts b/web/store/issue/project-views/filter.store.ts index c7c8988b1..050d3dce4 100644 --- a/web/store/issue/project-views/filter.store.ts +++ b/web/store/issue/project-views/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { ViewService } from "services/view.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { ViewService } from "services/view.service"; export interface IProjectViewIssuesFilter { // observables @@ -35,7 +35,7 @@ export interface IProjectViewIssuesFilter { projectId: string, filterType: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - viewId?: string | undefined + viewId: string ) => Promise; } @@ -134,11 +134,9 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I projectId: string, type: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - viewId: string | undefined = undefined + viewId: string ) => { try { - if (!viewId) throw new Error("View id is required"); - if (isEmpty(this.filters) || isEmpty(this.filters[viewId]) || isEmpty(filters)) return; const _filters = { diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index b85465ec8..97cf537c7 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -1,13 +1,13 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { IssueService } from "services/issue/issue.service"; +import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService } from "services/issue/issue.service"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IProjectViewIssues { // observable @@ -21,33 +21,23 @@ export interface IProjectViewIssues { workspaceSlug: string, projectId: string, loadType: TLoader, - viewId?: string | undefined + viewId: string ) => Promise; createIssue: ( workspaceSlug: string, projectId: string, data: Partial, - viewId?: string | undefined + viewId: string ) => Promise; updateIssue: ( workspaceSlug: string, projectId: string, issueId: string, data: Partial, - viewId?: string | undefined - ) => Promise; - removeIssue: ( - workspaceSlug: string, - projectId: string, - issueId: string, - viewId?: string | undefined - ) => Promise; - archiveIssue: ( - workspaceSlug: string, - projectId: string, - issueId: string, - viewId?: string | undefined + viewId: string ) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string, viewId: string) => Promise; + archiveIssue: (workspaceSlug: string, projectId: string, issueId: string, viewId: string) => Promise; quickAddIssue: ( workspaceSlug: string, projectId: string, @@ -124,15 +114,8 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI return issues; } - fetchIssues = async ( - workspaceSlug: string, - projectId: string, - loadType: TLoader = "init-loader", - viewId: string | undefined = undefined - ) => { + fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader", viewId: string) => { try { - if (!viewId) throw new Error("View Id is required"); - this.loader = loadType; const params = this.rootIssueStore?.projectViewIssuesFilter?.appliedFilters; @@ -157,15 +140,8 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI } }; - createIssue = async ( - workspaceSlug: string, - projectId: string, - data: Partial, - viewId: string | undefined = undefined - ) => { + createIssue = async (workspaceSlug: string, projectId: string, data: Partial, viewId: string) => { try { - if (!viewId) throw new Error("View Id is required"); - const response = await this.rootIssueStore.projectIssues.createIssue(workspaceSlug, projectId, data); runInAction(() => { @@ -174,7 +150,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI return response; } catch (error) { - this.fetchIssues(workspaceSlug, projectId, "mutation"); + this.fetchIssues(workspaceSlug, projectId, "mutation", viewId); throw error; } }; @@ -184,27 +160,18 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI projectId: string, issueId: string, data: Partial, - viewId: string | undefined = undefined + viewId: string ) => { try { - if (!viewId) throw new Error("View Id is required"); - await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); } catch (error) { - this.fetchIssues(workspaceSlug, projectId, "mutation"); + this.fetchIssues(workspaceSlug, projectId, "mutation", viewId); throw error; } }; - removeIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - viewId: string | undefined = undefined - ) => { + removeIssue = async (workspaceSlug: string, projectId: string, issueId: string, viewId: string) => { try { - if (!viewId) throw new Error("View Id is required"); - await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); const issueIndex = this.issues[viewId].findIndex((_issueId) => _issueId === issueId); @@ -213,27 +180,20 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI this.issues[viewId].splice(issueIndex, 1); }); } catch (error) { - this.fetchIssues(workspaceSlug, projectId, "mutation"); + this.fetchIssues(workspaceSlug, projectId, "mutation", viewId); throw error; } }; - archiveIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - viewId: string | undefined = undefined - ) => { + archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string, viewId: string) => { try { - if (!viewId) throw new Error("View Id is required"); - await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); runInAction(() => { pull(this.issues[viewId], issueId); }); } catch (error) { - this.fetchIssues(workspaceSlug, projectId, "mutation"); + this.fetchIssues(workspaceSlug, projectId, "mutation", viewId); throw error; } }; @@ -263,7 +223,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI return response; } catch (error) { - this.fetchIssues(workspaceSlug, projectId, "mutation"); + if (viewId) this.fetchIssues(workspaceSlug, projectId, "mutation", viewId); throw error; } }; diff --git a/web/store/issue/project/filter.store.ts b/web/store/issue/project/filter.store.ts index f18654cde..d5c353487 100644 --- a/web/store/issue/project/filter.store.ts +++ b/web/store/issue/project/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { IssueFiltersService } from "services/issue_filter.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -17,10 +15,12 @@ import { IIssueFilters, TIssueParams, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { IssueFiltersService } from "services/issue_filter.service"; export interface IProjectIssuesFilter { // observables @@ -189,7 +189,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj updatedDisplayFilters.group_by = "state"; } - runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index f3ee94783..080b8cee6 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -1,15 +1,15 @@ -import { action, makeObservable, observable, runInAction, computed } from "mobx"; +import concat from "lodash/concat"; +import pull from "lodash/pull"; import set from "lodash/set"; import update from "lodash/update"; -import pull from "lodash/pull"; -import concat from "lodash/concat"; +import { action, makeObservable, observable, runInAction, computed } from "mobx"; // base class +import { IssueService, IssueArchiveService } from "services/issue"; +import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService, IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IProjectIssues { // observable diff --git a/web/store/issue/root.store.ts b/web/store/issue/root.store.ts index def91d200..68206a704 100644 --- a/web/store/issue/root.store.ts +++ b/web/store/issue/root.store.ts @@ -1,28 +1,28 @@ -import { autorun, makeObservable, observable } from "mobx"; import isEmpty from "lodash/isEmpty"; +import { autorun, makeObservable, observable } from "mobx"; // root store +import { IWorkspaceMembership } from "store/member/workspace-member.store"; +import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; import { RootStore } from "../root.store"; import { IStateStore, StateStore } from "../state.store"; // issues data store -import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; -import { IIssueStore, IssueStore } from "./issue.store"; +import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived"; +import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle"; +import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft"; import { IIssueDetail, IssueDetail } from "./issue-details/root.store"; -import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace"; +import { IIssueStore, IssueStore } from "./issue.store"; +import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store"; +import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store"; +import { IModuleIssuesFilter, ModuleIssuesFilter, IModuleIssues, ModuleIssues } from "./module"; import { IProfileIssuesFilter, ProfileIssuesFilter, IProfileIssues, ProfileIssues } from "./profile"; import { IProjectIssuesFilter, ProjectIssuesFilter, IProjectIssues, ProjectIssues } from "./project"; -import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle"; -import { IModuleIssuesFilter, ModuleIssuesFilter, IModuleIssues, ModuleIssues } from "./module"; import { IProjectViewIssuesFilter, ProjectViewIssuesFilter, IProjectViewIssues, ProjectViewIssues, } from "./project-views"; -import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived"; -import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft"; -import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store"; -import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store"; -import { IWorkspaceMembership } from "store/member/workspace-member.store"; +import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace"; export interface IIssueRootStore { currentUserId: string | undefined; @@ -35,6 +35,7 @@ export interface IIssueRootStore { userId: string | undefined; // user profile detail Id stateMap: Record | undefined; stateDetails: IState[] | undefined; + workspaceStateDetails: IState[] | undefined; labelMap: Record | undefined; workSpaceMemberRolesMap: Record | undefined; memberMap: Record | undefined; @@ -89,6 +90,7 @@ export class IssueRootStore implements IIssueRootStore { userId: string | undefined = undefined; stateMap: Record | undefined = undefined; stateDetails: IState[] | undefined = undefined; + workspaceStateDetails: IState[] | undefined = undefined; labelMap: Record | undefined = undefined; workSpaceMemberRolesMap: Record | undefined = undefined; memberMap: Record | undefined = undefined; @@ -142,6 +144,7 @@ export class IssueRootStore implements IIssueRootStore { globalViewId: observable.ref, stateMap: observable, stateDetails: observable, + workspaceStateDetails: observable, labelMap: observable, memberMap: observable, workSpaceMemberRolesMap: observable, @@ -163,6 +166,7 @@ export class IssueRootStore implements IIssueRootStore { if (rootStore.app.router.userId) this.userId = rootStore.app.router.userId; if (!isEmpty(rootStore?.state?.stateMap)) this.stateMap = rootStore?.state?.stateMap; if (!isEmpty(rootStore?.state?.projectStates)) this.stateDetails = rootStore?.state?.projectStates; + if (!isEmpty(rootStore?.state?.workspaceStates)) this.workspaceStateDetails = rootStore?.state?.workspaceStates; if (!isEmpty(rootStore?.label?.labelMap)) this.labelMap = rootStore?.label?.labelMap; if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap)) this.workSpaceMemberRolesMap = rootStore?.memberRoot?.workspace?.memberMap || undefined; diff --git a/web/store/issue/workspace/filter.store.ts b/web/store/issue/workspace/filter.store.ts index 76b861f4b..8278bdfc7 100644 --- a/web/store/issue/workspace/filter.store.ts +++ b/web/store/issue/workspace/filter.store.ts @@ -1,14 +1,12 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import isEmpty from "lodash/isEmpty"; -import set from "lodash/set"; -import pickBy from "lodash/pickBy"; import isArray from "lodash/isArray"; +import isEmpty from "lodash/isEmpty"; +import pickBy from "lodash/pickBy"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; // base class -import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; -// helpers +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -// types -import { IIssueRootStore } from "../root.store"; +import { WorkspaceService } from "services/workspace.service"; import { IIssueFilterOptions, IIssueDisplayFilterOptions, @@ -18,10 +16,12 @@ import { TIssueParams, TStaticViewTypes, } from "@plane/types"; +import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; +// helpers +// types +import { IIssueRootStore } from "../root.store"; // constants -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; // services -import { WorkspaceService } from "services/workspace.service"; type TWorkspaceFilters = "all-issues" | "assigned" | "created" | "subscribed" | string; export interface IWorkspaceIssuesFilter { @@ -37,7 +37,7 @@ export interface IWorkspaceIssuesFilter { projectId: string | undefined, filterType: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - viewId?: string | undefined + viewId: string ) => Promise; //helper action getIssueFilters: (viewId: string | undefined) => IIssueFilters | undefined; @@ -156,10 +156,9 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo projectId: string | undefined, type: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - viewId: string | undefined = undefined + viewId: string ) => { try { - if (!viewId) throw new Error("View id is required"); const issueFilters = this.getIssueFilters(viewId); if (!issueFilters || isEmpty(filters)) return; diff --git a/web/store/issue/workspace/issue.store.ts b/web/store/issue/workspace/issue.store.ts index b7fe43b30..7d7e52e1e 100644 --- a/web/store/issue/workspace/issue.store.ts +++ b/web/store/issue/workspace/issue.store.ts @@ -1,14 +1,14 @@ -import { action, observable, makeObservable, computed, runInAction } from "mobx"; -import set from "lodash/set"; import pull from "lodash/pull"; +import set from "lodash/set"; +import { action, observable, makeObservable, computed, runInAction } from "mobx"; // base class +import { IssueService, IssueArchiveService } from "services/issue"; +import { WorkspaceService } from "services/workspace.service"; +import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { WorkspaceService } from "services/workspace.service"; -import { IssueService, IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; -import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; export interface IWorkspaceIssues { // observable @@ -23,27 +23,23 @@ export interface IWorkspaceIssues { workspaceSlug: string, projectId: string, data: Partial, - viewId?: string | undefined + viewId: string ) => Promise; updateIssue: ( workspaceSlug: string, projectId: string, issueId: string, data: Partial, - viewId?: string | undefined - ) => Promise; - removeIssue: ( - workspaceSlug: string, - projectId: string, - issueId: string, - viewId?: string | undefined + viewId: string ) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string, viewId: string) => Promise; archiveIssue: ( workspaceSlug: string, projectId: string, issueId: string, viewId?: string | undefined ) => Promise; + quickAddIssue: undefined; } export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssues { @@ -61,6 +57,8 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue issueService; issueArchiveService; + quickAddIssue = undefined; + constructor(_rootStore: IIssueRootStore) { super(_rootStore); @@ -139,15 +137,8 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue } }; - createIssue = async ( - workspaceSlug: string, - projectId: string, - data: Partial, - viewId: string | undefined = undefined - ) => { + createIssue = async (workspaceSlug: string, projectId: string, data: Partial, viewId: string) => { try { - if (!viewId) throw new Error("View id is required"); - const uniqueViewId = `${workspaceSlug}_${viewId}`; const response = await this.issueService.createIssue(workspaceSlug, projectId, data); @@ -169,11 +160,9 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue projectId: string, issueId: string, data: Partial, - viewId: string | undefined = undefined + viewId: string ) => { try { - if (!viewId) throw new Error("View id is required"); - this.rootStore.issues.updateIssue(issueId, data); await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); } catch (error) { @@ -182,15 +171,8 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue } }; - removeIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - viewId: string | undefined = undefined - ) => { + removeIssue = async (workspaceSlug: string, projectId: string, issueId: string, viewId: string) => { try { - if (!viewId) throw new Error("View id is required"); - const uniqueViewId = `${workspaceSlug}_${viewId}`; await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); diff --git a/web/store/label.store.ts b/web/store/label.store.ts index 769ef16a9..386676dfe 100644 --- a/web/store/label.store.ts +++ b/web/store/label.store.ts @@ -1,11 +1,11 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services +import { buildTree } from "helpers/array.helper"; import { IssueLabelService } from "services/issue"; // helpers -import { buildTree } from "helpers/array.helper"; // types import { RootStore } from "store/root.store"; import { IIssueLabel, IIssueLabelTree } from "@plane/types"; diff --git a/web/store/member/index.ts b/web/store/member/index.ts index a7eba3971..d43398d0b 100644 --- a/web/store/member/index.ts +++ b/web/store/member/index.ts @@ -2,8 +2,8 @@ import { action, makeObservable, observable } from "mobx"; // types import { RootStore } from "store/root.store"; import { IUserLite } from "@plane/types"; -import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store"; import { IProjectMemberStore, ProjectMemberStore } from "./project-member.store"; +import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store"; export interface IMemberRootStore { // observables diff --git a/web/store/member/project-member.store.ts b/web/store/member/project-member.store.ts index 71e2e2dcd..6cb39e2ef 100644 --- a/web/store/member/project-member.store.ts +++ b/web/store/member/project-member.store.ts @@ -1,17 +1,17 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services +import { EUserProjectRoles } from "constants/project"; import { ProjectMemberService } from "services/project"; // types +import { IRouterStore } from "store/application/router.store"; import { RootStore } from "store/root.store"; +import { IUserRootStore } from "store/user"; import { IProjectBulkAddFormData, IProjectMember, IProjectMembership, IUserLite } from "@plane/types"; // constants -import { EUserProjectRoles } from "constants/project"; import { IMemberRootStore } from "."; -import { IRouterStore } from "store/application/router.store"; -import { IUserRootStore } from "store/user"; interface IProjectMemberDetails { id: string; diff --git a/web/store/member/workspace-member.store.ts b/web/store/member/workspace-member.store.ts index 4a696bfd2..b1472a9d2 100644 --- a/web/store/member/workspace-member.store.ts +++ b/web/store/member/workspace-member.store.ts @@ -1,17 +1,17 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services +import { EUserWorkspaceRoles } from "constants/workspace"; import { WorkspaceService } from "services/workspace.service"; // types +import { IRouterStore } from "store/application/router.store"; import { RootStore } from "store/root.store"; +import { IUserRootStore } from "store/user"; import { IWorkspaceBulkInviteFormData, IWorkspaceMember, IWorkspaceMemberInvitation } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; -import { IRouterStore } from "store/application/router.store"; import { IMemberRootStore } from "."; -import { IUserRootStore } from "store/user"; export interface IWorkspaceMembership { id: string; @@ -127,9 +127,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { const searchedWorkspaceMemberIds = workspaceMemberIds?.filter((userId) => { const memberDetails = this.getWorkspaceMemberDetails(userId); if (!memberDetails) return false; - const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${ - memberDetails.member.display_name - } ${memberDetails.member.email ?? ""}`; + const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${memberDetails + .member?.display_name} ${memberDetails.member.email ?? ""}`; return memberSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase()); }); return searchedWorkspaceMemberIds; diff --git a/web/store/mention.store.ts b/web/store/mention.store.ts index 872efeb41..5cfa0478a 100644 --- a/web/store/mention.store.ts +++ b/web/store/mention.store.ts @@ -1,6 +1,6 @@ +import { IMentionHighlight, IMentionSuggestion } from "@plane/lite-text-editor"; import { computed, makeObservable } from "mobx"; // editor -import { IMentionHighlight, IMentionSuggestion } from "@plane/lite-text-editor"; // types import { RootStore } from "store/root.store"; diff --git a/web/store/module.store.ts b/web/store/module.store.ts index 2b4522cd0..c7dcba79c 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -1,13 +1,13 @@ -import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // services -import { ProjectService } from "services/project"; import { ModuleService } from "services/module.service"; +import { ProjectService } from "services/project"; // types -import { IModule, ILinkDetails } from "@plane/types"; import { RootStore } from "store/root.store"; +import { IModule, ILinkDetails } from "@plane/types"; export interface IModuleStore { //Loaders diff --git a/web/store/page.store.ts b/web/store/page.store.ts index fa5970e49..30fc3d157 100644 --- a/web/store/page.store.ts +++ b/web/store/page.store.ts @@ -1,7 +1,7 @@ import { action, makeObservable, observable, reaction, runInAction } from "mobx"; -import { IIssueLabel, IPage } from "@plane/types"; import { PageService } from "services/page.service"; +import { IIssueLabel, IPage } from "@plane/types"; import { RootStore } from "./root.store"; @@ -35,7 +35,7 @@ export interface IPageStore { addToFavorites: () => Promise; removeFromFavorites: () => Promise; updateName: (name: string) => Promise; - updateDescription: (description: string) => Promise; + updateDescription: (description: string) => void; // Reactions disposers: Array<() => void>; @@ -89,7 +89,7 @@ export class PageStore implements IPageStore { addToFavorites: action, removeFromFavorites: action, updateName: action, - updateDescription: action, + updateDescription: action.bound, setIsSubmitting: action, cleanup: action, }); @@ -166,7 +166,7 @@ export class PageStore implements IPageStore { this.name = name; }); - updateDescription = action("updateDescription", async (description_html: string) => { + updateDescription = action("updateDescription", (description_html: string) => { const { projectId, workspaceSlug } = this.rootStore.app.router; if (!projectId || !workspaceSlug) return; diff --git a/web/store/project-page.store.ts b/web/store/project-page.store.ts index 072605bc3..c16e8ab08 100644 --- a/web/store/project-page.store.ts +++ b/web/store/project-page.store.ts @@ -1,5 +1,6 @@ -import { makeObservable, observable, runInAction, action, computed } from "mobx"; +import { isThisWeek, isToday, isYesterday } from "date-fns"; import { set } from "lodash"; +import { makeObservable, observable, runInAction, action, computed } from "mobx"; // services import { PageService } from "services/page.service"; // store @@ -7,7 +8,6 @@ import { PageStore, IPageStore } from "store/page.store"; // types import { IPage, IRecentPages } from "@plane/types"; import { RootStore } from "./root.store"; -import { isThisWeek, isToday, isYesterday } from "date-fns"; export interface IProjectPageStore { loader: boolean; diff --git a/web/store/project/index.ts b/web/store/project/index.ts index 696b3c802..dff0db175 100644 --- a/web/store/project/index.ts +++ b/web/store/project/index.ts @@ -1,6 +1,6 @@ -import { IProjectStore, ProjectStore } from "./project.store"; -import { IProjectPublishStore, ProjectPublishStore } from "./project-publish.store"; import { RootStore } from "store/root.store"; +import { IProjectPublishStore, ProjectPublishStore } from "./project-publish.store"; +import { IProjectStore, ProjectStore } from "./project.store"; export interface IProjectRootStore { project: IProjectStore; diff --git a/web/store/project/project-publish.store.ts b/web/store/project/project-publish.store.ts index 3a94b8611..9be1cb48c 100644 --- a/web/store/project/project-publish.store.ts +++ b/web/store/project/project-publish.store.ts @@ -1,9 +1,9 @@ -import { observable, action, makeObservable, runInAction } from "mobx"; import set from "lodash/set"; +import { observable, action, makeObservable, runInAction } from "mobx"; // types +import { ProjectPublishService } from "services/project"; import { ProjectRootStore } from "./"; // services -import { ProjectPublishService } from "services/project"; export type TProjectPublishViews = "list" | "gantt" | "kanban" | "calendar" | "spreadsheet"; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index c9aa826fe..1b9220a2d 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -1,14 +1,14 @@ -import { observable, action, computed, makeObservable, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; +import { cloneDeep, update } from "lodash"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // types -import { RootStore } from "../root.store"; -import { IProject } from "@plane/types"; -// services import { IssueLabelService, IssueService } from "services/issue"; import { ProjectService, ProjectStateService } from "services/project"; -import { cloneDeep, update } from "lodash"; +import { IProject } from "@plane/types"; +import { RootStore } from "../root.store"; +// services export interface IProjectStore { // observables searchQuery: string; @@ -148,7 +148,7 @@ export class ProjectStore implements IProjectStore { projects = sortBy(projects, "created_at"); const projectIds = projects - .filter((project) => project.workspace === currentWorkspace.id && project.is_favorite) + .filter((project) => project.workspace === currentWorkspace.id && project.is_member && project.is_favorite) .map((project) => project.id); return projectIds; } diff --git a/web/store/root.store.ts b/web/store/root.store.ts index 3e0733249..298cd532e 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -1,23 +1,23 @@ import { enableStaticRendering } from "mobx-react-lite"; // root stores import { AppRootStore, IAppRootStore } from "./application"; -import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; -import { IProjectRootStore, ProjectRootStore } from "./project"; import { CycleStore, ICycleStore } from "./cycle.store"; -import { IProjectViewStore, ProjectViewStore } from "./project-view.store"; +import { DashboardStore, IDashboardStore } from "./dashboard.store"; +import { IEstimateStore, EstimateStore } from "./estimate.store"; +import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; +import { GlobalViewStore, IGlobalViewStore } from "./global-view.store"; +import { IInboxRootStore, InboxRootStore } from "./inbox/root.store"; +import { IssueRootStore, IIssueRootStore } from "./issue/root.store"; +import { ILabelStore, LabelStore } from "./label.store"; +import { IMemberRootStore, MemberRootStore } from "./member"; +import { IMentionStore, MentionStore } from "./mention.store"; import { IModuleStore, ModulesStore } from "./module.store"; +import { IProjectRootStore, ProjectRootStore } from "./project"; +import { IProjectViewStore, ProjectViewStore } from "./project-view.store"; +import { IStateStore, StateStore } from "./state.store"; import { IUserRootStore, UserRootStore } from "./user"; import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace"; -import { IssueRootStore, IIssueRootStore } from "./issue/root.store"; -import { IInboxRootStore, InboxRootStore } from "./inbox/root.store"; -import { IStateStore, StateStore } from "./state.store"; -import { IMemberRootStore, MemberRootStore } from "./member"; -import { IEstimateStore, EstimateStore } from "./estimate.store"; -import { GlobalViewStore, IGlobalViewStore } from "./global-view.store"; -import { IMentionStore, MentionStore } from "./mention.store"; -import { DashboardStore, IDashboardStore } from "./dashboard.store"; import { IProjectPageStore, ProjectPageStore } from "./project-page.store"; -import { ILabelStore, LabelStore } from "./label.store"; enableStaticRendering(typeof window === "undefined"); diff --git a/web/store/state.store.ts b/web/store/state.store.ts index 783a82ee2..eaece6db0 100644 --- a/web/store/state.store.ts +++ b/web/store/state.store.ts @@ -1,15 +1,15 @@ -import { makeObservable, observable, computed, action, runInAction } from "mobx"; -import { computedFn } from "mobx-utils"; import groupBy from "lodash/groupBy"; import set from "lodash/set"; +import { makeObservable, observable, computed, action, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; // store +import { sortStates } from "helpers/state.helper"; +import { ProjectStateService } from "services/project"; +import { IState } from "@plane/types"; import { RootStore } from "./root.store"; // types -import { IState } from "@plane/types"; // services -import { ProjectStateService } from "services/project"; // helpers -import { sortStates } from "helpers/state.helper"; export interface IStateStore { //Loaders @@ -17,6 +17,7 @@ export interface IStateStore { // observables stateMap: Record; // computed + workspaceStates: IState[] | undefined; projectStates: IState[] | undefined; groupedProjectStates: Record | undefined; // computed actions @@ -73,13 +74,22 @@ export class StateStore implements IStateStore { this.router = _rootStore.app.router; } + /** + * Returns the stateMap belongs to a specific workspace + */ + get workspaceStates() { + const workspaceSlug = this.router.workspaceSlug || ""; + if (!workspaceSlug || !this.fetchedMap[workspaceSlug]) return; + return sortStates(Object.values(this.stateMap)); + } + /** * Returns the stateMap belongs to a specific project */ get projectStates() { const projectId = this.router.projectId; - const worksapceSlug = this.router.workspaceSlug || ""; - if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return; + const workspaceSlug = this.router.workspaceSlug || ""; + if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return; return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); } @@ -106,8 +116,8 @@ export class StateStore implements IStateStore { * @returns IState[] */ getProjectStates = computedFn((projectId: string) => { - const worksapceSlug = this.router.workspaceSlug || ""; - if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return; + const workspaceSlug = this.router.workspaceSlug || ""; + if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[workspaceSlug])) return; return sortStates(Object.values(this.stateMap).filter((state) => state.project_id === projectId)); }); diff --git a/web/store/user/index.ts b/web/store/user/index.ts index 15f9e5772..1a94e16b3 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -1,7 +1,7 @@ import { action, observable, runInAction, makeObservable } from "mobx"; // services -import { UserService } from "services/user.service"; import { AuthService } from "services/auth.service"; +import { UserService } from "services/user.service"; // interfaces import { IUser, IUserSettings } from "@plane/types"; // store @@ -22,7 +22,6 @@ export interface IUserRootStore { fetchCurrentUser: () => Promise; fetchCurrentUserInstanceAdminStatus: () => Promise; fetchCurrentUserSettings: () => Promise; - fetchUserDashboardInfo: (workspaceSlug: string, month: number) => Promise; // crud actions updateUserOnBoard: () => Promise; updateTourCompleted: () => Promise; @@ -68,7 +67,6 @@ export class UserRootStore implements IUserRootStore { fetchCurrentUser: action, fetchCurrentUserInstanceAdminStatus: action, fetchCurrentUserSettings: action, - fetchUserDashboardInfo: action, updateUserOnBoard: action, updateTourCompleted: action, updateCurrentUser: action, @@ -130,22 +128,6 @@ export class UserRootStore implements IUserRootStore { return response; }); - /** - * Fetches the current user dashboard info - * @returns Promise - */ - fetchUserDashboardInfo = async (workspaceSlug: string, month: number) => { - try { - const response = await this.userService.userWorkspaceDashboard(workspaceSlug, month); - runInAction(() => { - this.dashboardInfo = response; - }); - return response; - } catch (error) { - throw error; - } - }; - /** * Updates the user onboarding status * @returns Promise diff --git a/web/store/user/user-membership.store.ts b/web/store/user/user-membership.store.ts index b8bdbfac5..a1f5c1b81 100644 --- a/web/store/user/user-membership.store.ts +++ b/web/store/user/user-membership.store.ts @@ -1,6 +1,8 @@ -import { action, observable, runInAction, makeObservable, computed } from "mobx"; import { set } from "lodash"; +import { action, observable, runInAction, makeObservable, computed } from "mobx"; // services +import { EUserProjectRoles } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; import { ProjectMemberService } from "services/project"; import { UserService } from "services/user.service"; import { WorkspaceService } from "services/workspace.service"; @@ -8,8 +10,6 @@ import { WorkspaceService } from "services/workspace.service"; import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole } from "@plane/types"; import { RootStore } from "../root.store"; // constants -import { EUserProjectRoles } from "constants/project"; -import { EUserWorkspaceRoles } from "constants/workspace"; export interface IUserMembershipStore { // observables diff --git a/web/store/workspace/api-token.store.ts b/web/store/workspace/api-token.store.ts index f0772933d..351ead561 100644 --- a/web/store/workspace/api-token.store.ts +++ b/web/store/workspace/api-token.store.ts @@ -2,9 +2,9 @@ import { action, observable, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; import { APITokenService } from "services/api_token.service"; +import { IApiToken } from "@plane/types"; import { RootStore } from "../root.store"; // types -import { IApiToken } from "@plane/types"; export interface IApiTokenStore { // observables diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts index 4020aaef7..863982e1a 100644 --- a/web/store/workspace/index.ts +++ b/web/store/workspace/index.ts @@ -1,13 +1,13 @@ -import { action, computed, observable, makeObservable, runInAction } from "mobx"; -import { RootStore } from "../root.store"; import set from "lodash/set"; -// types -import { IWorkspace } from "@plane/types"; -// services +import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { WorkspaceService } from "services/workspace.service"; +import { IWorkspace } from "@plane/types"; +import { RootStore } from "../root.store"; +// types +// services // sub-stores -import { IWebhookStore, WebhookStore } from "./webhook.store"; import { ApiTokenStore, IApiTokenStore } from "./api-token.store"; +import { IWebhookStore, WebhookStore } from "./webhook.store"; export interface IWorkspaceRootStore { // observables diff --git a/web/store/workspace/webhook.store.ts b/web/store/workspace/webhook.store.ts index 5657f341e..256b41e38 100644 --- a/web/store/workspace/webhook.store.ts +++ b/web/store/workspace/webhook.store.ts @@ -1,8 +1,8 @@ // mobx import { action, observable, makeObservable, computed, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { IWebhook } from "@plane/types"; import { WebhookService } from "services/webhook.service"; +import { IWebhook } from "@plane/types"; import { RootStore } from "../root.store"; export interface IWebhookStore { diff --git a/web/styles/emoji.css b/web/styles/emoji.css new file mode 100644 index 000000000..2fe3ddab3 --- /dev/null +++ b/web/styles/emoji.css @@ -0,0 +1,52 @@ +.EmojiPickerReact { + --epr-category-navigation-button-size: 1.25rem !important; + --epr-category-label-height: 1.5rem !important; + --epr-emoji-size: 1.25rem !important; + --epr-picker-border-radius: 0.25rem !important; + --epr-horizontal-padding: 0.5rem !important; + --epr-emoji-padding: 0.5rem !important; + background-color: rgba(var(--color-background-100)) !important; +} + +.epr-main { + border: none !important; + border-radius: 0 !important; +} + +.epr-emoji-category-label { + font-size: 0.7875rem !important; + color: rgba(var(--color-text-300)) !important; + background-color: rgba(var(--color-background-100), 0.8) !important; +} + +.epr-category-nav, +.epr-header-overlay { + padding: 0.5rem !important; +} + +button.epr-emoji:hover > *, +button.epr-emoji:focus > * { + background-color: rgba(var(--color-background-80)) !important; +} + +input.epr-search { + font-size: 0.7875rem !important; + height: 2rem !important; + background: transparent !important; + border-color: rgba(var(--color-border-200)) !important; + border-radius: 0.25rem !important; +} + +input.epr-search::placeholder { + color: rgba(var(--color-text-400)) !important; +} + +button.epr-btn-clear-search:hover { + background-color: rgba(var(--color-background-80)) !important; + color: rgba(var(--color-text-300)) !important; +} + +.epr-emoji-variation-picker { + background-color: rgba(var(--color-background-100)) !important; + border-color: rgba(var(--color-border-200)) !important; +} diff --git a/web/styles/globals.css b/web/styles/globals.css index e4de1a3da..6c51e75c4 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -149,6 +149,27 @@ --color-onboarding-border-300: 229, 229, 229, 0.5; --color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(126, 139, 171, 0.1); + + /* toast theme */ + --color-toast-success-text: 62, 155, 79; + --color-toast-error-text: 220, 62, 66; + --color-toast-warning-text: 255, 186, 24; + --color-toast-info-text: 51, 88, 212; + --color-toast-loading-text: 28, 32, 36; + --color-toast-secondary-text: 128, 131, 141; + --color-toast-tertiary-text: 96, 100, 108; + + --color-toast-success-background: 253, 253, 254; + --color-toast-error-background: 255, 252, 252; + --color-toast-warning-background: 254, 253, 251; + --color-toast-info-background: 253, 253, 254; + --color-toast-loading-background: 253, 253, 254; + + --color-toast-success-border: 218, 241, 219; + --color-toast-error-border: 255, 219, 220; + --color-toast-warning-border: 255, 247, 194; + --color-toast-info-border: 210, 222, 255; + --color-toast-loading-border: 224, 225, 230; } [data-theme="light-contrast"] { @@ -217,6 +238,27 @@ --color-onboarding-border-300: 34, 35, 38, 0.5; --color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(39, 44, 56, 0.1); + + /* toast theme */ + --color-toast-success-text: 178, 221, 181; + --color-toast-error-text: 206, 44, 49; + --color-toast-warning-text: 255, 186, 24; + --color-toast-info-text: 141, 164, 239; + --color-toast-loading-text: 255, 255, 255; + --color-toast-secondary-text: 185, 187, 198; + --color-toast-tertiary-text: 139, 141, 152; + + --color-toast-success-background: 46, 46, 46; + --color-toast-error-background: 46, 46, 46; + --color-toast-warning-background: 46, 46, 46; + --color-toast-info-background: 46, 46, 46; + --color-toast-loading-background: 46, 46, 46; + + --color-toast-success-border: 42, 126, 59; + --color-toast-error-border: 100, 23, 35; + --color-toast-warning-border: 79, 52, 34; + --color-toast-info-border: 58, 91, 199; + --color-toast-loading-border: 96, 100, 108; } [data-theme="dark-contrast"] { diff --git a/yarn.lock b/yarn.lock index f413d1a44..66fef83d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,13 +29,6 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" @@ -269,7 +262,7 @@ "@babel/traverse" "^7.23.6" "@babel/types" "^7.23.6" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.23.4": +"@babel/highlight@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== @@ -1288,37 +1281,7 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== - dependencies: - ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" - -"@eslint/eslintrc@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.4.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/eslintrc@^2.0.1", "@eslint/eslintrc@^2.1.4": +"@eslint/eslintrc@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== @@ -1333,15 +1296,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.36.0": - version "8.36.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" - integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== - -"@eslint/js@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" - integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== "@floating-ui/core@^1.4.2": version "1.5.2" @@ -1399,38 +1357,24 @@ redux "^4.2.1" use-memo-one "^1.1.3" -"@humanwhocodes/config-array@^0.11.13", "@humanwhocodes/config-array@^0.11.8": - version "0.11.13" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" - integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^2.0.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" minimatch "^3.0.5" -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== - dependencies: - "@humanwhocodes/object-schema" "^1.2.0" - debug "^4.1.1" - minimatch "^3.0.4" - "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@humanwhocodes/object-schema@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" - integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" + integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== "@hypnosphi/create-react-context@^0.3.1": version "0.3.1" @@ -1591,33 +1535,12 @@ resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== -"@next/eslint-plugin-next@12.2.2": - version "12.2.2" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.2.2.tgz#b4a22c06b6454068b54cc44502168d90fbb29a6d" - integrity sha512-XOi0WzJhGH3Lk51SkSu9eZxF+IY1ZZhWcJTIGBycAbWU877IQa6+6KxMATWCOs7c+bmp6Sd8KywXJaDRxzu0JA== +"@next/eslint-plugin-next@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.0.tgz#29b041233fac7417e22eefa4146432d5cd910820" + integrity sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q== dependencies: - glob "7.1.7" - -"@next/eslint-plugin-next@13.0.0": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.0.0.tgz#cf3d799b21671554c1f5889c01d2513afb9973cd" - integrity sha512-z+gnX4Zizatqatc6f4CQrcC9oN8Us3Vrq/OLyc98h7K/eWctrnV91zFZodmJHUjx0cITY8uYM7LXD7IdYkg3kg== - dependencies: - glob "7.1.7" - -"@next/eslint-plugin-next@13.2.1": - version "13.2.1" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.1.tgz#58dea4d53c0adfc59c10195f51eb8d3575fce414" - integrity sha512-r0i5rcO6SMAZtqiGarUVMr3k256X0R0j6pEkKg4PxqUW+hG0qgMxRVAJsuoRG5OBFkCOlSfWZJ0mP9fQdCcyNg== - dependencies: - glob "7.1.7" - -"@next/eslint-plugin-next@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.4.tgz#3e124cd10ce24dab5d3448ce04104b4f1f4c6ca7" - integrity sha512-ck1lI+7r1mMJpqLNa3LJ5pxCfOB1lfJncKmRJeJxcJqcngaFwylreLP7da6Rrjr6u2gVRTfmnkSkjc80IiQCwQ== - dependencies: - glob "7.1.7" + glob "10.3.10" "@next/swc-darwin-arm64@14.0.4": version "14.0.4" @@ -1766,19 +1689,6 @@ "@react-spring/web" "9.4.5" d3-shape "^1.3.5" -"@nivo/marimekko@0.80.0": - version "0.80.0" - resolved "https://registry.yarnpkg.com/@nivo/marimekko/-/marimekko-0.80.0.tgz#1eda4207935b3776bd1d7d729dc39a0e42bb1b86" - integrity sha512-0u20SryNtbOQhtvhZsbxmgnF7o8Yc2rjDQ/gCYPTXtxeooWCuhSaRZbDCnCeyKQY3B62D7z2mu4Js4KlTEftjA== - dependencies: - "@nivo/axes" "0.80.0" - "@nivo/colors" "0.80.0" - "@nivo/legends" "0.80.0" - "@nivo/scales" "0.80.0" - "@react-spring/web" "9.4.5" - d3-shape "^1.3.5" - lodash "^4.17.21" - "@nivo/pie@0.80.0": version "0.80.0" resolved "https://registry.yarnpkg.com/@nivo/pie/-/pie-0.80.0.tgz#04b35839bf5a2b661fa4e5b677ae76b3c028471e" @@ -2212,10 +2122,10 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.1.tgz#d146db7a5949e10837b323ce933ed882ac878262" integrity sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA== -"@rushstack/eslint-patch@^1.1.3": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz#9ab8f811930d7af3e3d549183a50884f9eb83f36" - integrity sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw== +"@rushstack/eslint-patch@^1.3.3": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" + integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== "@scena/dragscroll@^1.4.0": version "1.4.0" @@ -2896,16 +2806,16 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/eslint-plugin@^6.13.2": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz#cc29fbd208ea976de3db7feb07755bba0ce8d8bc" - integrity sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ== +"@typescript-eslint/eslint-plugin@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz#dd71fc5c7ecec745ca26ece506d84d203a205c0e" + integrity sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.16.0" - "@typescript-eslint/type-utils" "6.16.0" - "@typescript-eslint/utils" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/scope-manager" "7.1.1" + "@typescript-eslint/type-utils" "7.1.1" + "@typescript-eslint/utils" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -2913,14 +2823,26 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^5.21.0", "@typescript-eslint/parser@^5.42.0", "@typescript-eslint/parser@^5.48.2": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" - integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== +"@typescript-eslint/parser@^5.4.2 || ^6.0.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== dependencies: - "@typescript-eslint/scope-manager" "5.62.0" - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + +"@typescript-eslint/parser@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.1.1.tgz#6a9d0a5c9ccdf5dbd3cb8c949728c64e24e07d1f" + integrity sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ== + dependencies: + "@typescript-eslint/scope-manager" "7.1.1" + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/typescript-estree" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" debug "^4.3.4" "@typescript-eslint/scope-manager@5.62.0": @@ -2931,13 +2853,21 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz#f3e9a00fbc1d0701356359cd56489c54d9e37168" - integrity sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw== +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== dependencies: - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + +"@typescript-eslint/scope-manager@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz#9e301803ff8e21a74f50c6f89a4baccad9a48f93" + integrity sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA== + dependencies: + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" "@typescript-eslint/type-utils@5.62.0": version "5.62.0" @@ -2949,13 +2879,13 @@ debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/type-utils@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz#5f21c3e49e540ad132dc87fc99af463c184d5ed1" - integrity sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg== +"@typescript-eslint/type-utils@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz#aee820d5bedd39b83c18585a526cc520ddb7a226" + integrity sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g== dependencies: - "@typescript-eslint/typescript-estree" "6.16.0" - "@typescript-eslint/utils" "6.16.0" + "@typescript-eslint/typescript-estree" "7.1.1" + "@typescript-eslint/utils" "7.1.1" debug "^4.3.4" ts-api-utils "^1.0.1" @@ -2964,10 +2894,15 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.16.0.tgz#a3abe0045737d44d8234708d5ed8fef5d59dc91e" - integrity sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + +"@typescript-eslint/types@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.1.1.tgz#ca33ba7cf58224fb46a84fea62593c2c53cd795f" + integrity sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -2982,13 +2917,27 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz#d6e0578e4f593045f0df06c4b3a22bd6f13f2d03" - integrity sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/visitor-keys" "6.16.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/typescript-estree@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz#09c54af0151a1b05d0875c0fc7fe2ec7a2476ece" + integrity sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw== + dependencies: + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/visitor-keys" "7.1.1" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -3010,17 +2959,17 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/utils@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.16.0.tgz#1c291492d34670f9210d2b7fcf6b402bea3134ae" - integrity sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A== +"@typescript-eslint/utils@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.1.1.tgz#bdeeb789eee4af5d3fb5400a69566d4dbf97ff3b" + integrity sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.16.0" - "@typescript-eslint/types" "6.16.0" - "@typescript-eslint/typescript-estree" "6.16.0" + "@typescript-eslint/scope-manager" "7.1.1" + "@typescript-eslint/types" "7.1.1" + "@typescript-eslint/typescript-estree" "7.1.1" semver "^7.5.4" "@typescript-eslint/visitor-keys@5.62.0": @@ -3031,12 +2980,20 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.16.0": - version "6.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz#d50da18a05d91318ed3e7e8889bda0edc35f3a10" - integrity sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A== +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== dependencies: - "@typescript-eslint/types" "6.16.0" + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" + +"@typescript-eslint/visitor-keys@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz#e6538a58c9b157f03bcbb29e3b6a92fe39a6ab0d" + integrity sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ== + dependencies: + "@typescript-eslint/types" "7.1.1" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -3044,16 +3001,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: +acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - acorn@^8.8.2, acorn@^8.9.0: version "8.11.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" @@ -3071,7 +3023,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3081,7 +3033,7 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1, ajv@^8.6.0: +ajv@^8.6.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -3091,11 +3043,6 @@ ajv@^8.0.1, ajv@^8.6.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -3143,13 +3090,6 @@ arg@^5.0.2: resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -3177,7 +3117,7 @@ array-buffer-byte-length@^1.0.0: call-bind "^1.0.2" is-array-buffer "^3.0.1" -array-includes@^3.1.5, array-includes@^3.1.6, array-includes@^3.1.7: +array-includes@^3.1.6, array-includes@^3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== @@ -3226,7 +3166,7 @@ array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.0, array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: +array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== @@ -3265,11 +3205,6 @@ ast-types-flow@^0.0.8: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - async@^3.2.3: version "3.2.5" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" @@ -3922,7 +3857,7 @@ date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4129,6 +4064,11 @@ electron-to-chromium@^1.4.601: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz#4bddbc2c76e1e9dbf449ecd5da3d8119826ea4fb" integrity sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg== +emoji-picker-react@^4.5.16: + version "4.5.16" + resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.16.tgz#12111f89a7fd2bd74965337d53806f4153d65dc6" + integrity sha512-RXaOH1EapmqbtRSMaHnwJWMfA6kiPipg/gN4cFOQRQKvrTQIA3K5+yUyzFuq8O7umIEtXUi1C1tf2dPvyyn44Q== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -4159,14 +4099,6 @@ enhanced-resolve@^5.12.0: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5: - version "2.4.1" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" - integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== - dependencies: - ansi-colors "^4.1.1" - strip-ansi "^6.0.1" - entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -4445,77 +4377,32 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-next@12.2.2: - version "12.2.2" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.2.2.tgz#4bb996026e118071849bc4011283a160ad5bde46" - integrity sha512-oJhWBLC4wDYYUFv/5APbjHUFd0QRFCojMdj/QnMoOEktmeTvwnnoA8F8uaXs0fQgsaTK0tbUxBRv9/Y4/rpxOA== +eslint-config-next@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.1.0.tgz#7e309d426b8afacaba3b32fdbb02ba220b6d0a97" + integrity sha512-SBX2ed7DoRFXC6CQSLc/SbLY9Ut6HxNB2wPTcoIWjUMd7aF7O/SIE7111L8FdZ9TXsNV4pulUDnfthpyPtbFUg== dependencies: - "@next/eslint-plugin-next" "12.2.2" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.21.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^2.7.1" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.29.4" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-next@13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.0.0.tgz#d533ee1dbd6576fd3759ba4db4d5a6c4e039c242" - integrity sha512-y2nqWS2tycWySdVhb+rhp6CuDmDazGySqkzzQZf3UTyfHyC7og1m5m/AtMFwCo5mtvDqvw1BENin52kV9733lg== - dependencies: - "@next/eslint-plugin-next" "13.0.0" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.21.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^2.7.1" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-next@13.2.1: - version "13.2.1" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.2.1.tgz#644fb3496b832bc1e32f2c57cce1ec3eeb7bb7a1" - integrity sha512-2GAx7EjSiCzJN6H2L/v1kbYrNiwQxzkyjy6eWSjuhAKt+P6d3nVNHGy9mON8ZcYd72w/M8kyMjm4UB9cvijgrw== - dependencies: - "@next/eslint-plugin-next" "13.2.1" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" + "@next/eslint-plugin-next" "14.1.0" + "@rushstack/eslint-patch" "^1.3.3" + "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" eslint-import-resolver-node "^0.3.6" eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" + eslint-plugin-import "^2.28.1" + eslint-plugin-jsx-a11y "^6.7.1" + eslint-plugin-react "^7.33.2" + eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" -eslint-config-next@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.2.4.tgz#8aa4d42da3a575a814634ba9c88c8d25266c5fdd" - integrity sha512-lunIBhsoeqw6/Lfkd6zPt25w1bn0znLA/JCL+au1HoEpSb4/PpsOYsYtgV/q+YPsoKIOzFyU5xnb04iZnXjUvg== +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== + +eslint-config-turbo@^1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/eslint-config-turbo/-/eslint-config-turbo-1.12.4.tgz#b911aced2228e98176dbebe0f1ebef345a253400" + integrity sha512-5hqEaV6PNmAYLL4RTmq74OcCt8pgzOLnfDVPG/7PUXpQ0Mpz0gr926oCSFukywKKXjdum3VHD84S7Z9A/DqTAw== dependencies: - "@next/eslint-plugin-next" "13.2.4" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" - -eslint-config-prettier@^8.3.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" - integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== - -eslint-config-turbo@latest: - version "1.11.2" - resolved "https://registry.yarnpkg.com/eslint-config-turbo/-/eslint-config-turbo-1.11.2.tgz#8e6c456f58e88ecc9adface9c5e03fa782e8bba5" - integrity sha512-vqbyCH6kCHFoIAWUmGL61c0BfUQNz0XAl2RzAnEkSQ+PLXvEvuV2HsvL51UOzyyElfJlzZuh9T4BvUqb5KR9Eg== - dependencies: - eslint-plugin-turbo "1.11.2" + eslint-plugin-turbo "1.12.4" eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -4526,17 +4413,6 @@ eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-import-resolver-typescript@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz#a90a4a1c80da8d632df25994c4c5fdcdd02b8751" - integrity sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ== - dependencies: - debug "^4.3.4" - glob "^7.2.0" - is-glob "^4.0.3" - resolve "^1.22.0" - tsconfig-paths "^3.14.1" - eslint-import-resolver-typescript@^3.5.2: version "3.6.1" resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" @@ -4557,7 +4433,7 @@ eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.26.0: +eslint-plugin-import@^2.28.1, eslint-plugin-import@^2.29.1: version "2.29.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== @@ -4580,7 +4456,7 @@ eslint-plugin-import@^2.26.0: semver "^6.3.1" tsconfig-paths "^3.15.0" -eslint-plugin-jsx-a11y@^6.5.1: +eslint-plugin-jsx-a11y@^6.7.1: version "6.8.0" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== @@ -4602,32 +4478,12 @@ eslint-plugin-jsx-a11y@^6.5.1: object.entries "^1.1.7" object.fromentries "^2.0.7" -eslint-plugin-react-hooks@^4.5.0: +"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": version "4.6.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== -eslint-plugin-react@7.31.8: - version "7.31.8" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz#3a4f80c10be1bcbc8197be9e8b641b2a3ef219bf" - integrity sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw== - dependencies: - array-includes "^3.1.5" - array.prototype.flatmap "^1.3.0" - doctrine "^2.1.0" - estraverse "^5.3.0" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.1.2" - object.entries "^1.1.5" - object.fromentries "^2.0.5" - object.hasown "^1.1.1" - object.values "^1.1.5" - prop-types "^15.8.1" - resolve "^2.0.0-next.3" - semver "^6.3.0" - string.prototype.matchall "^4.0.7" - -eslint-plugin-react@^7.29.4, eslint-plugin-react@^7.31.7: +eslint-plugin-react@^7.33.2: version "7.33.2" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== @@ -4649,10 +4505,10 @@ eslint-plugin-react@^7.29.4, eslint-plugin-react@^7.31.7: semver "^6.3.1" string.prototype.matchall "^4.0.8" -eslint-plugin-turbo@1.11.2: - version "1.11.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-turbo/-/eslint-plugin-turbo-1.11.2.tgz#7bb450cced51d35369a678114c2ee9882937adc5" - integrity sha512-U6DX+WvgGFiwEAqtOjm4Ejd9O4jsw8jlFNkQi0ywxbMnbiTie+exF4Z0F/B1ajtjjeZkBkgRnlU+UkoraBN+bw== +eslint-plugin-turbo@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-turbo/-/eslint-plugin-turbo-1.12.4.tgz#f29ddd89cb853db5dd4332db39ec2d85c713041e" + integrity sha512-3AGmXvH7E4i/XTWqBrcgu+G7YKZJV/8FrEn79kTd50ilNsv+U3nS2IlcCrQB6Xm2m9avGD9cadLzKDR1/UF2+g== dependencies: dotenv "16.0.3" @@ -4664,7 +4520,7 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.1, eslint-scope@^7.2.2: +eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== @@ -4672,182 +4528,21 @@ eslint-scope@^7.1.1, eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@8.34.0: - version "8.34.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6" - integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== - dependencies: - "@eslint/eslintrc" "^1.4.1" - "@humanwhocodes/config-array" "^0.11.8" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -eslint@8.36.0: - version "8.36.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf" - integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.1" - "@eslint/js" "8.36.0" - "@humanwhocodes/config-array" "^0.11.8" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-visitor-keys "^3.3.0" - espree "^9.5.0" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -eslint@^7.23.0, eslint@^7.32.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.0.1" - doctrine "^3.0.0" - enquirer "^2.3.5" - escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.9" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -eslint@^8.31.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" - integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== +eslint@^8.57.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.56.0" - "@humanwhocodes/config-array" "^0.11.13" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" "@ungap/structured-clone" "^1.2.0" @@ -4882,16 +4577,7 @@ eslint@^8.31.0: strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== - dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" - -espree@^9.4.0, espree@^9.5.0, espree@^9.6.0, espree@^9.6.1: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -4900,12 +4586,7 @@ espree@^9.4.0, espree@^9.5.0, espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.0, esquery@^1.4.2: +esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== @@ -5175,11 +4856,6 @@ function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: es-abstract "^1.22.1" functions-have-names "^1.2.3" -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -5262,19 +4938,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^10.3.10: +glob@10.3.10, glob@^10.3.10: version "10.3.10" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== @@ -5285,7 +4949,7 @@ glob@^10.3.10: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" -glob@^7.0.3, glob@^7.1.3, glob@^7.1.6, glob@^7.2.0: +glob@^7.0.3, glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5313,7 +4977,7 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0, globals@^13.6.0, globals@^13.9.0: +globals@^13.19.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== @@ -5362,11 +5026,6 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -5476,11 +5135,6 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.2.0, ignore@^5.2.4: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" @@ -5491,7 +5145,7 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -5890,24 +5544,11 @@ js-cookie@^3.0.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== -js-sdsl@^4.1.4: - version "4.4.2" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847" - integrity sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -6156,11 +5797,6 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== - lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -6590,7 +6226,7 @@ minimatch@9.0.3, minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -6832,7 +6468,7 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.5, object.entries@^1.1.6, object.entries@^1.1.7: +object.entries@^1.1.6, object.entries@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== @@ -6841,7 +6477,7 @@ object.entries@^1.1.5, object.entries@^1.1.6, object.entries@^1.1.7: define-properties "^1.2.0" es-abstract "^1.22.1" -object.fromentries@^2.0.5, object.fromentries@^2.0.6, object.fromentries@^2.0.7: +object.fromentries@^2.0.6, object.fromentries@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== @@ -6860,7 +6496,7 @@ object.groupby@^1.0.1: es-abstract "^1.22.1" get-intrinsic "^1.2.1" -object.hasown@^1.1.1, object.hasown@^1.1.2: +object.hasown@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== @@ -6882,7 +6518,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.5, object.values@^1.1.6, object.values@^1.1.7: +object.values@^1.1.6, object.values@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== @@ -6905,7 +6541,7 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -optionator@^0.9.1, optionator@^0.9.3: +optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== @@ -7234,7 +6870,7 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== -progress@^2.0.0, progress@^2.0.3: +progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -7737,11 +7373,6 @@ regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: define-properties "^1.2.0" set-function-name "^2.0.0" -regexpp@^3.1.0, regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" @@ -7800,7 +7431,7 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.0, resolve@^1.22.2, resolve@^1.22.4: +resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.2, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -7809,7 +7440,7 @@ resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.3, resolve@^2.0.0-next.4: +resolve@^2.0.0-next.4: version "2.0.0-next.5" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== @@ -7965,12 +7596,12 @@ selecto@~1.26.3: keycon "^1.2.0" overlap-area "^1.1.0" -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.2.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: +semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -8090,15 +7721,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -8107,6 +7729,11 @@ snake-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" +sonner@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.4.2.tgz#92740c293e9d911de726080995bd8a0cc677ccd1" + integrity sha512-x3Kfzfhb56V/ErvUnH5dZcsu6QkZpyIlRAogO4vAbN+AkBsA/8CFqOV+5djqbE5pQCpejtO4JBWL1zRj2sO/Vg== + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -8152,11 +7779,6 @@ space-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - stacktrace-parser@^0.1.10: version "0.1.10" resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" @@ -8177,7 +7799,8 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8195,7 +7818,7 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.7, string.prototype.matchall@^4.0.8: +string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== @@ -8254,6 +7877,7 @@ stringify-object@^3.3.0: is-regexp "^1.0.0" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8282,7 +7906,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -8363,17 +7987,6 @@ tabbable@^6.0.1: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== -table@^6.0.9: - version "6.8.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" - integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tailwind-merge@^1.14.0: version "1.14.0" resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b" @@ -8540,10 +8153,10 @@ tippy.js@^6.3.1, tippy.js@^6.3.7: dependencies: "@popperjs/core" "^2.9.0" -tiptap-markdown@^0.8.2: - version "0.8.8" - resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.8.tgz#1e25f40b726239dff84b99a53eb1bdf4af0a02f9" - integrity sha512-I2w/IpvCZ1BoR3nQzG0wRK3uGmDv+Ohyr++G24Ma6RzoDYd0TVGXZp0BOODX5Jj4c6heVY8eksahSeAwJMZBeg== +tiptap-markdown@^0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.9.tgz#e13f3ae9a1b1649f8c28bb3cae4516a53da7492c" + integrity sha512-TykSDcsb94VFCzPbSSTfB6Kh2HJi7x4B9J3Jm9uSOAMPy8App1YfrLW/rEJLajTxwMVhWBdOo4nidComSlLQsQ== dependencies: "@types/markdown-it" "^12.2.3" markdown-it "^13.0.1" @@ -8599,7 +8212,7 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tsconfig-paths@^3.14.1, tsconfig-paths@^3.15.0: +tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== @@ -8796,11 +8409,16 @@ typescript@4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== -typescript@4.9.5, typescript@^4.7.4: +typescript@4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -9001,11 +8619,6 @@ uvu@^0.5.0: kleur "^4.0.3" sade "^1.7.3" -v8-compile-cache@^2.0.3: - version "2.4.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" - integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== - vfile-message@^3.0.0: version "3.1.4" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea"