diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml new file mode 100644 index 00000000000..58c404e376e --- /dev/null +++ b/.github/workflows/build-branch.yml @@ -0,0 +1,213 @@ + +name: Branch Build + +on: + pull_request: + types: + - closed + branches: + - master + - release + - qa + - develop + +env: + TARGET_BRANCH: ${{ github.event.pull_request.base.ref }} + +jobs: + branch_build_and_push: + if: ${{ (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) }} + name: Build-Push Web/Space/API/Proxy Docker Image + runs-on: ubuntu-20.04 + + steps: + - name: Check out the repo + uses: actions/checkout@v3.3.0 + + # - name: Set Target Branch Name on PR close + # if: ${{ github.event_name == 'pull_request' && github.event.action =='closed' }} + # run: echo "TARGET_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV + + # - name: Set Target Branch Name on other than PR close + # if: ${{ github.event_name == 'push' }} + # run: echo "TARGET_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + + - uses: ASzc/change-string-case-action@v2 + id: gh_branch_upper_lower + with: + string: ${{env.TARGET_BRANCH}} + + - uses: mad9000/actions-find-and-replace-string@2 + id: gh_branch_replace_slash + with: + source: ${{ steps.gh_branch_upper_lower.outputs.lowercase }} + find: '/' + replace: '-' + + - uses: mad9000/actions-find-and-replace-string@2 + id: gh_branch_replace_dot + with: + source: ${{ steps.gh_branch_replace_slash.outputs.value }} + find: '.' + replace: '' + + - uses: mad9000/actions-find-and-replace-string@2 + id: gh_branch_clean + with: + source: ${{ steps.gh_branch_replace_dot.outputs.value }} + find: '_' + replace: '' + - name: Uploading Proxy Source + uses: actions/upload-artifact@v3 + with: + name: proxy-src-code + path: ./nginx + - name: Uploading Backend Source + uses: actions/upload-artifact@v3 + with: + name: backend-src-code + path: ./apiserver + - name: Uploading Web Source + uses: actions/upload-artifact@v3 + with: + name: web-src-code + path: | + ./ + !./apiserver + !./nginx + !./deploy + !./space + + - name: Uploading Space Source + uses: actions/upload-artifact@v3 + with: + name: space-src-code + path: | + ./ + !./apiserver + !./nginx + !./deploy + !./web + outputs: + gh_branch_name: ${{ steps.gh_branch_clean.outputs.value }} + + branch_build_push_frontend: + runs-on: ubuntu-20.04 + needs: [ branch_build_and_push ] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Downloading Web Source Code + uses: actions/download-artifact@v3 + with: + name: web-src-code + + - name: Build and Push Frontend to Docker Container Registry + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./web/Dockerfile.web + platforms: linux/amd64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_space: + runs-on: ubuntu-20.04 + needs: [ branch_build_and_push ] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Downloading Space Source Code + uses: actions/download-artifact@v3 + with: + name: space-src-code + + - name: Build and Push Space to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./space/Dockerfile.space + platforms: linux/amd64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_backend: + runs-on: ubuntu-20.04 + needs: [ branch_build_and_push ] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Downloading Backend Source Code + uses: actions/download-artifact@v3 + with: + name: backend-src-code + + - name: Build and Push Backend to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./Dockerfile.api + platforms: linux/amd64 + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }} + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + + branch_build_push_proxy: + runs-on: ubuntu-20.04 + needs: [ branch_build_and_push ] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Login to Docker Hub + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Downloading Proxy Source Code + uses: actions/download-artifact@v3 + with: + name: proxy-src-code + + - name: Build and Push Plane-Proxy to Docker Hub + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }} + push: true + env: + DOCKER_BUILDKIT: 1 + DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 7568602d31f..dcb8b86712e 100644 --- a/.gitignore +++ b/.gitignore @@ -75,7 +75,7 @@ pnpm-lock.yaml pnpm-workspace.yaml .npmrc +.secrets tmp/ - ## packages dist diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b25a791d080..73d69fb2d5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,8 @@ Before submitting a new issue, please search the [issues](https://github.com/mak While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like: -- 3rd-party libraries being used and their versions -- a use-case that fails +- 3rd-party libraries being used and their versions +- a use-case that fails Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved. @@ -19,10 +19,10 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla ### Requirements -- Node.js version v16.18.0 -- Python version 3.8+ -- Postgres version v14 -- Redis version v6.2.7 +- Node.js version v16.18.0 +- Python version 3.8+ +- Postgres version v14 +- Redis version v6.2.7 ### Setup the project @@ -81,8 +81,8 @@ If you would like to _implement_ it, an issue with your proposal must be submitt To ensure consistency throughout the source code, please keep these rules in mind as you are working: -- All features or bug fixes must be tested by one or more specs (unit-tests). -- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier. +- All features or bug fixes must be tested by one or more specs (unit-tests). +- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier. ## Need help? Questions and suggestions @@ -90,11 +90,11 @@ Questions, suggestions, and thoughts are most welcome. We can also be reached in ## Ways to contribute -- Try Plane Cloud and the self hosting platform and give feedback -- Add new integrations -- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose) -- Share your thoughts and suggestions with us -- Help create tutorials and blog posts -- Request a feature by submitting a proposal -- Report a bug -- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations. +- Try Plane Cloud and the self hosting platform and give feedback +- Add new integrations +- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose) +- Share your thoughts and suggestions with us +- Help create tutorials and blog posts +- Request a feature by submitting a proposal +- Report a bug +- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations. diff --git a/ENV_SETUP.md b/ENV_SETUP.md index 6796c3db6b1..23faf83f7c1 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -1,8 +1,10 @@ # Environment Variables + ​ -Environment variables are distributed in various files. Please refer them carefully. +Environment variables are distributed in various files. Please refer them carefully. ## {PROJECT_FOLDER}/.env + File is available in the project root folder​ ``` @@ -41,25 +43,37 @@ USE_MINIO=1 # Nginx Configuration NGINX_PORT=80 ``` + ​ + ## {PROJECT_FOLDER}/web/.env.example + ​ + ``` # Enable/Disable OAUTH - default 0 for selfhosted instance NEXT_PUBLIC_ENABLE_OAUTH=0 # Public boards deploy URL NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces" ``` + ​ + ## {PROJECT_FOLDER}/spaces/.env.example + ​ + ``` # Flag to toggle OAuth NEXT_PUBLIC_ENABLE_OAUTH=0 ``` + ​ + ## {PROJECT_FOLDER}/apiserver/.env + ​ + ``` # Backend # Debug value for api server use it as 0 for production use @@ -126,7 +140,9 @@ ENABLE_SIGNUP="1" # Email Redirection URL WEB_URL="http://localhost" ``` + ## Updates​ + - The environment variable NEXT_PUBLIC_API_BASE_URL has been removed from both the web and space projects. - The naming convention for containers and images has been updated. - The plane-worker image will no longer be maintained, as it has been merged with plane-backend. diff --git a/apiserver/.env.example b/apiserver/.env.example index 8193b5e7716..d3ad596e559 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -70,3 +70,6 @@ ENABLE_MAGIC_LINK_LOGIN="0" # Email redirections and minio domain settings WEB_URL="http://localhost" + +# Gunicorn Workers +GUNICORN_WORKERS=2 diff --git a/apiserver/bin/takeoff b/apiserver/bin/takeoff index dc25a14e2d1..9b09f244ea4 100755 --- a/apiserver/bin/takeoff +++ b/apiserver/bin/takeoff @@ -6,4 +6,4 @@ python manage.py migrate # Create a Default User python bin/user_script.py -exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - +exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index f061a0a1938..ae033969fff 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -5,7 +5,7 @@ from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer from .state import StateSerializer, StateLiteSerializer from .project import ProjectLiteSerializer @@ -548,7 +548,7 @@ class Meta: ] -class IssueLiteSerializer(BaseSerializer): +class IssueLiteSerializer(DynamicBaseSerializer): workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") project_detail = ProjectLiteSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state") diff --git a/apiserver/plane/api/serializers/state.py b/apiserver/plane/api/serializers/state.py index 097bc4c931f..ad416c340dc 100644 --- a/apiserver/plane/api/serializers/state.py +++ b/apiserver/plane/api/serializers/state.py @@ -7,8 +7,6 @@ class StateSerializer(BaseSerializer): - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") - project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: model = State diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py index 49c2b772e19..e4f3718f59c 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/api/urls/__init__.py @@ -1,7 +1,7 @@ from .analytic import urlpatterns as analytic_urls from .asset import urlpatterns as asset_urls from .authentication import urlpatterns as authentication_urls -from .configuration import urlpatterns as configuration_urls +from .config import urlpatterns as configuration_urls from .cycle import urlpatterns as cycle_urls from .estimate import urlpatterns as estimate_urls from .gpt import urlpatterns as gpt_urls diff --git a/apiserver/plane/api/urls/configuration.py b/apiserver/plane/api/urls/config.py similarity index 100% rename from apiserver/plane/api/urls/configuration.py rename to apiserver/plane/api/urls/config.py diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index f1ef7c1767e..23a8e4fa665 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -3,6 +3,8 @@ from plane.api.views import ( IssueViewSet, + IssueListEndpoint, + IssueListGroupedEndpoint, LabelViewSet, BulkCreateIssueLabelsEndpoint, BulkDeleteIssuesEndpoint, @@ -35,6 +37,16 @@ ), name="project-issue", ), + path( + "v2/workspaces//projects//issues/", + IssueListEndpoint.as_view(), + name="project-issue", + ), + path( + "v3/workspaces//projects//issues/", + IssueListGroupedEndpoint.as_view(), + name="project-issue", + ), path( "workspaces//projects//issues//", IssueViewSet.as_view( diff --git a/apiserver/plane/api/urls/state.py b/apiserver/plane/api/urls/state.py index bcfd80cd7cd..94aa55f24af 100644 --- a/apiserver/plane/api/urls/state.py +++ b/apiserver/plane/api/urls/state.py @@ -20,11 +20,19 @@ StateViewSet.as_view( { "get": "retrieve", - "put": "update", "patch": "partial_update", "delete": "destroy", } ), name="project-state", ), + path( + "workspaces//projects//states//mark-default/", + StateViewSet.as_view( + { + "post": "mark_as_default", + } + ), + name="project-state", + ), ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 8f4b2fb9d98..ca66ce48e8b 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -54,7 +54,12 @@ LeaveWorkspaceEndpoint, ) from .state import StateViewSet -from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet +from .view import ( + GlobalViewViewSet, + GlobalViewIssuesViewSet, + IssueViewViewSet, + IssueViewFavoriteViewSet, +) from .cycle import ( CycleViewSet, CycleIssueViewSet, @@ -65,6 +70,8 @@ from .asset import FileAssetEndpoint, UserAssetsEndpoint from .issue import ( IssueViewSet, + IssueListEndpoint, + IssueListGroupedEndpoint, WorkSpaceIssuesEndpoint, IssueActivityEndpoint, IssueCommentViewSet, @@ -162,7 +169,11 @@ DefaultAnalyticsEndpoint, ) -from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet +from .notification import ( + NotificationViewSet, + UnreadNotificationEndpoint, + MarkAllReadNotificationViewSet, +) from .exporter import ExportIssuesEndpoint diff --git a/apiserver/plane/api/views/config.py b/apiserver/plane/api/views/config.py index f59ca04a053..d035c4740ae 100644 --- a/apiserver/plane/api/views/config.py +++ b/apiserver/plane/api/views/config.py @@ -21,8 +21,8 @@ class ConfigurationEndpoint(BaseAPIView): def get(self, request): data = {} - data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None) - data["github"] = os.environ.get("GITHUB_CLIENT_ID", None) + data["google_client_id"] = os.environ.get("GOOGLE_CLIENT_ID", None) + data["github_client_id"] = os.environ.get("GITHUB_CLIENT_ID", None) data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None) data["magic_login"] = ( bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD) @@ -30,4 +30,8 @@ def get(self, request): data["email_password_login"] = ( os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1" ) + data["slack_client_id"] = os.environ.get("SLACK_CLIENT_ID", None) + data["posthog_api_key"] = os.environ.get("POSTHOG_API_KEY", None) + data["posthog_host"] = os.environ.get("POSTHOG_HOST", None) + data["has_unsplash_configured"] = bool(settings.UNSPLASH_ACCESS_KEY) return Response(data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index e7d247872ed..21defcc1339 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -176,9 +176,8 @@ def get_queryset(self): def list(self, request, slug, project_id): queryset = self.get_queryset() cycle_view = request.GET.get("cycle_view", "all") - order_by = request.GET.get("order_by", "sort_order") - queryset = queryset.order_by(order_by) + queryset = queryset.order_by("-is_favorite","-created_at") # Current Cycle if cycle_view == "current": @@ -479,13 +478,13 @@ def destroy(self, request, slug, project_id, pk): ) ) cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) - # Delete the cycle - cycle.delete() + issue_activity.delay( type="cycle.activity.deleted", requested_data=json.dumps( { "cycle_id": str(pk), + "cycle_name": str(cycle.name), "issues": [str(issue_id) for issue_id in cycle_issues], } ), @@ -495,6 +494,8 @@ def destroy(self, request, slug, project_id, pk): current_instance=None, epoch=int(timezone.now().timestamp()), ) + # Delete the cycle + cycle.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -511,12 +512,6 @@ class CycleIssueViewSet(BaseViewSet): "issue__assignees__id", ] - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - cycle_id=self.kwargs.get("cycle_id"), - ) - def get_queryset(self): return self.filter_queryset( super() @@ -669,7 +664,7 @@ def create(self, request, slug, project_id, cycle_id): type="cycle.activity.created", requested_data=json.dumps({"cycles_list": issues}), actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), + issue_id=None, project_id=str(self.kwargs.get("project_id", None)), current_instance=json.dumps( { diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 65b94d0a1e5..cc911b53716 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -1,6 +1,6 @@ # Python improts import uuid - +import requests # Django imports from django.contrib.auth.hashers import make_password @@ -25,7 +25,7 @@ delete_github_installation, ) from plane.api.permissions import WorkSpaceAdminPermission - +from plane.utils.integrations.slack import slack_oauth class IntegrationViewSet(BaseViewSet): serializer_class = IntegrationSerializer @@ -98,12 +98,19 @@ def create(self, request, slug, provider): config = {"installation_id": installation_id} if provider == "slack": - metadata = request.data.get("metadata", {}) + 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": "Access token and team id is required"}, + {"error": "Slack could not be installed. Please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) config = {"team_id": team_id, "access_token": access_token} diff --git a/apiserver/plane/api/views/integration/slack.py b/apiserver/plane/api/views/integration/slack.py index 83aa951baca..6b1b47d3748 100644 --- a/apiserver/plane/api/views/integration/slack.py +++ b/apiserver/plane/api/views/integration/slack.py @@ -11,6 +11,7 @@ from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember from plane.api.serializers import SlackProjectSyncSerializer from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission +from plane.utils.integrations.slack import slack_oauth class SlackProjectSyncViewSet(BaseViewSet): @@ -32,25 +33,47 @@ def get_queryset(self): ) def create(self, request, slug, project_id, workspace_integration_id): - serializer = SlackProjectSyncSerializer(data=request.data) + try: + code = request.data.get("code", False) - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) + if not code: + return Response( + {"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST + ) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - workspace_integration_id=workspace_integration_id, + 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) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + 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/api/views/issue.py b/apiserver/plane/api/views/issue.py index ff7f526915c..d1cd93e73ab 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -312,6 +312,104 @@ def destroy(self, request, slug, project_id, pk=None): return Response(status=status.HTTP_204_NO_CONTENT) +class IssueListEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + def get(self, request, slug, project_id): + fields = [field for field in request.GET.get("fields", "").split(",") if field] + filters = issue_filters(request.query_params, "GET") + + issue_queryset = ( + Issue.objects.filter(workspace__slug=slug, project_id=project_id) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .distinct() + ) + + serializer = IssueLiteSerializer( + issue_queryset, many=True, fields=fields if fields else None + ) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class IssueListGroupedEndpoint(BaseAPIView): + + permission_classes = [ + ProjectEntityPermission, + ] + + def get(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + fields = [field for field in request.GET.get("fields", "").split(",") if field] + + issue_queryset = ( + Issue.objects.filter(workspace__slug=slug, project_id=project_id) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .distinct() + ) + + issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data + issue_dict = {str(issue["id"]): issue for issue in issues} + return Response( + issue_dict, + status=status.HTTP_200_OK, + ) + + class UserWorkSpaceIssues(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug): @@ -2230,7 +2328,7 @@ def retrieve(self, request, slug, project_id, pk=None): def destroy(self, request, slug, project_id, pk=None): issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) current_instance = json.dumps( - IssueSerializer(current_instance).data, cls=DjangoJSONEncoder + IssueSerializer(issue).data, cls=DjangoJSONEncoder ) issue.delete() issue_activity.delay( diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 48f892764c5..6c208892204 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -55,7 +55,6 @@ def get_serializer_class(self): ) def get_queryset(self): - order_by = self.request.GET.get("order_by", "sort_order") subquery = ModuleFavorite.objects.filter( user=self.request.user, @@ -138,7 +137,7 @@ def get_queryset(self): ), ) ) - .order_by(order_by, "name") + .order_by("-is_favorite","-created_at") ) def create(self, request, slug, project_id): @@ -266,12 +265,12 @@ def destroy(self, request, slug, project_id, pk): module_issues = list( ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True) ) - module.delete() issue_activity.delay( type="module.activity.deleted", requested_data=json.dumps( { "module_id": str(pk), + "module_name": str(module.name), "issues": [str(issue_id) for issue_id in module_issues], } ), @@ -281,6 +280,7 @@ def destroy(self, request, slug, project_id, pk): current_instance=None, epoch=int(timezone.now().timestamp()), ) + module.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -297,12 +297,6 @@ class ModuleIssueViewSet(BaseViewSet): ProjectEntityPermission, ] - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - module_id=self.kwargs.get("module_id"), - ) - def get_queryset(self): return self.filter_queryset( super() @@ -446,7 +440,7 @@ def create(self, request, slug, project_id, module_id): type="module.activity.created", requested_data=json.dumps({"modules_list": issues}), actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), + issue_id=None, project_id=str(self.kwargs.get("project_id", None)), current_instance=json.dumps( { diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 063abf0e312..dbb6e1d7172 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -47,36 +47,45 @@ def create(self, request, slug, project_id): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def list(self, request, slug, project_id): - state_dict = dict() states = StateSerializer(self.get_queryset(), many=True).data - - for key, value in groupby( - sorted(states, key=lambda state: state["group"]), - lambda state: state.get("group"), - ): - state_dict[str(key)] = list(value) - - return Response(state_dict, status=status.HTTP_200_OK) + grouped = request.GET.get("grouped", False) + if grouped == "true": + state_dict = {} + for key, value in groupby( + sorted(states, key=lambda state: state["group"]), + lambda state: state.get("group"), + ): + state_dict[str(key)] = list(value) + return Response(state_dict, status=status.HTTP_200_OK) + return Response(states, status=status.HTTP_200_OK) + + def mark_as_default(self, request, slug, project_id, pk): + # Select all the states which are marked as default + _ = State.objects.filter( + workspace__slug=slug, project_id=project_id, default=True + ).update(default=False) + _ = State.objects.filter( + workspace__slug=slug, project_id=project_id, pk=pk + ).update(default=True) + return Response(status=status.HTTP_204_NO_CONTENT) def destroy(self, request, slug, project_id, pk): state = State.objects.get( ~Q(name="Triage"), - pk=pk, project_id=project_id, workspace__slug=slug, + pk=pk, + project_id=project_id, + workspace__slug=slug, ) if state.default: - return Response( - {"error": "Default state cannot be deleted"}, status=False - ) + return Response({"error": "Default state cannot be deleted"}, status=False) # Check for any issues in the state issue_exist = Issue.issue_objects.filter(state=pk).exists() if issue_exist: return Response( - { - "error": "The state is not empty, only empty states can be deleted" - }, + {"error": "The state is not empty, only empty states can be deleted"}, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index f0a20eeec39..4776bceab9d 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -691,6 +691,10 @@ def create_cycle_issue_activity( new_cycle = Cycle.objects.filter( pk=updated_record.get("new_cycle_id", None) ).first() + issue = Issue.objects.filter(pk=updated_record.get("issue_id")).first() + if issue: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( @@ -713,6 +717,10 @@ def create_cycle_issue_activity( cycle = Cycle.objects.filter( pk=created_record.get("fields").get("cycle") ).first() + issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() + if issue: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( @@ -747,22 +755,27 @@ def delete_cycle_issue_activity( ) cycle_id = requested_data.get("cycle_id", "") + cycle_name = requested_data.get("cycle_name", "") cycle = Cycle.objects.filter(pk=cycle_id).first() issues = requested_data.get("issues") for issue in issues: + current_issue = Issue.objects.filter(pk=issue).first() + if issue: + current_issue.updated_at = timezone.now() + current_issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( issue_id=issue, actor_id=actor_id, verb="deleted", - old_value=cycle.name if cycle is not None else "", + old_value=cycle.name if cycle is not None else cycle_name, new_value="", field="cycles", project_id=project_id, workspace_id=workspace_id, - comment=f"removed this issue from {cycle.name if cycle is not None else None}", - old_identifier=cycle.id if cycle is not None else None, + comment=f"removed this issue from {cycle.name if cycle is not None else cycle_name}", + old_identifier=cycle_id if cycle_id is not None else None, epoch=epoch, ) ) @@ -794,6 +807,10 @@ def create_module_issue_activity( new_module = Module.objects.filter( pk=updated_record.get("new_module_id", None) ).first() + issue = Issue.objects.filter(pk=updated_record.get("issue_id")).first() + if issue: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( @@ -816,6 +833,10 @@ def create_module_issue_activity( module = Module.objects.filter( pk=created_record.get("fields").get("module") ).first() + issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() + if issue: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( issue_id=created_record.get("fields").get("issue"), @@ -849,22 +870,27 @@ def delete_module_issue_activity( ) module_id = requested_data.get("module_id", "") + module_name = requested_data.get("module_name", "") module = Module.objects.filter(pk=module_id).first() issues = requested_data.get("issues") for issue in issues: + current_issue = Issue.objects.filter(pk=issue).first() + if issue: + current_issue.updated_at = timezone.now() + current_issue.save(update_fields=["updated_at"]) issue_activities.append( IssueActivity( issue_id=issue, actor_id=actor_id, verb="deleted", - old_value=module.name if module is not None else "", + old_value=module.name if module is not None else module_name, new_value="", field="modules", project_id=project_id, workspace_id=workspace_id, - comment=f"removed this issue from ", - old_identifier=module.id if module is not None else None, + comment=f"removed this issue from {module.name if module is not None else module_name}", + old_identifier=module_id if module_id is not None else None, epoch=epoch, ) ) @@ -1452,15 +1478,16 @@ def issue_activity( issue_activities = [] project = Project.objects.get(pk=project_id) - issue = Issue.objects.filter(pk=issue_id).first() workspace_id = project.workspace_id - if issue is not None: - try: - issue.updated_at = timezone.now() - issue.save(update_fields=["updated_at"]) - except Exception as e: - pass + if issue_id is not None: + issue = Issue.objects.filter(pk=issue_id).first() + if issue: + try: + issue.updated_at = timezone.now() + issue.save(update_fields=["updated_at"]) + except Exception as e: + pass ACTIVITY_MAPPER = { "issue.activity.created": create_issue_activity, diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index 4380f4ee90e..0c2199e447e 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -1,8 +1,6 @@ # Python imports import json - -# Django imports -from django.utils import timezone +import uuid # Module imports from plane.db.models import ( @@ -14,6 +12,7 @@ Issue, Notification, IssueComment, + IssueActivity ) # Third Party imports @@ -21,12 +20,35 @@ from bs4 import BeautifulSoup + +# =========== Issue Description Html Parsing and Notification Functions ====================== + +def update_mentions_for_issue(issue, project, new_mentions, removed_mention): + aggregated_issue_mentions = [] + + for mention_id in new_mentions: + aggregated_issue_mentions.append( + IssueMention( + mention_id=mention_id, + issue=issue, + project=project, + workspace_id=project.workspace_id + ) + ) + + IssueMention.objects.bulk_create( + aggregated_issue_mentions, batch_size=100) + IssueMention.objects.filter( + issue=issue, mention__in=removed_mention).delete() + + def get_new_mentions(requested_instance, current_instance): # requested_data is the newer instance of the current issue # current_instance is the older instance of the current issue, saved in the database # extract mentions from both the instance of data mentions_older = extract_mentions(current_instance) + mentions_newer = extract_mentions(requested_instance) # Getting Set Difference from mentions_newer @@ -64,25 +86,26 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions): # If the particular mention has not already been subscribed to the issue, he must be sent the mentioned notification if not IssueSubscriber.objects.filter( issue_id=issue_id, - subscriber=mention_id, - project=project_id, + subscriber_id=mention_id, + project_id=project_id, + ).exists() and not IssueAssignee.objects.filter( + project_id=project_id, issue_id=issue_id, + assignee_id=mention_id + ).exists() and not Issue.objects.filter( + project_id=project_id, pk=issue_id, created_by_id=mention_id ).exists(): - mentioned_user = User.objects.get(pk=mention_id) project = Project.objects.get(pk=project_id) - issue = Issue.objects.get(pk=issue_id) bulk_mention_subscribers.append(IssueSubscriber( - workspace=project.workspace, - project=project, - issue=issue, - subscriber=mentioned_user, + workspace_id=project.workspace_id, + project_id=project_id, + issue_id=issue_id, + subscriber_id=mention_id, )) return bulk_mention_subscribers # Parse Issue Description & extracts mentions - - def extract_mentions(issue_instance): try: # issue_instance has to be a dictionary passed, containing the description_html and other set of activity data. @@ -99,6 +122,65 @@ def extract_mentions(issue_instance): return list(set(mentions)) except Exception as e: return [] + + +# =========== Comment Parsing and Notification Functions ====================== +def extract_comment_mentions(comment_value): + try: + mentions = [] + soup = BeautifulSoup(comment_value, 'html.parser') + 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: + return [] + +def get_new_comment_mentions(new_value, old_value): + + mentions_newer = extract_comment_mentions(new_value) + if old_value is None: + return mentions_newer + + mentions_older = extract_comment_mentions(old_value) + # Getting Set Difference from mentions_newer + new_mentions = [ + mention for mention in mentions_newer if mention not in mentions_older] + + return new_mentions + + +def createMentionNotification(project, notification_comment, issue, actor_id, mention_id, issue_id, activity): + return Notification( + workspace=project.workspace, + sender="in_app:issue_activities:mentioned", + triggered_by_id=actor_id, + receiver_id=mention_id, + entity_identifier=issue_id, + entity_name="issue", + project=project, + message=notification_comment, + data={ + "issue": { + "id": str(issue_id), + "name": str(issue.name), + "identifier": str(issue.project.identifier), + "sequence_id": issue.sequence_id, + "state_name": issue.state.name, + "state_group": issue.state.group, + }, + "issue_activity": { + "id": str(activity.get("id")), + "verb": str(activity.get("verb")), + "field": str(activity.get("field")), + "actor": str(activity.get("actor_id")), + "new_value": str(activity.get("new_value")), + "old_value": str(activity.get("old_value")), + } + }, + ) @shared_task @@ -126,61 +208,97 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi bulk_notifications = [] """ - Mention Tasks - 1. Perform Diffing and Extract the mentions, that mention notification needs to be sent - 2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers - """ + Mention Tasks + 1. Perform Diffing and Extract the mentions, that mention notification needs to be sent + 2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers + """ # Get new mentions from the newer instance new_mentions = get_new_mentions( requested_instance=requested_data, current_instance=current_instance) removed_mention = get_removed_mentions( requested_instance=requested_data, current_instance=current_instance) + + comment_mentions = [] + all_comment_mentions = [] # Get New Subscribers from the mentions of the newer instance requested_mentions = extract_mentions( issue_instance=requested_data) mention_subscribers = extract_mentions_as_subscribers( project_id=project_id, issue_id=issue_id, mentions=requested_mentions) - - issue_subscribers = list( - IssueSubscriber.objects.filter( - project_id=project_id, issue_id=issue_id) - .exclude(subscriber_id__in=list(new_mentions + [actor_id])) - .values_list("subscriber", flat=True) - ) - + + for issue_activity in issue_activities_created: + issue_comment = issue_activity.get("issue_comment") + issue_comment_new_value = issue_activity.get("new_value") + issue_comment_old_value = issue_activity.get("old_value") + if issue_comment is not None: + # TODO: Maybe save the comment mentions, so that in future, we can filter out the issues based on comment mentions as well. + + all_comment_mentions = all_comment_mentions + extract_comment_mentions(issue_comment_new_value) + + new_comment_mentions = get_new_comment_mentions(old_value=issue_comment_old_value, new_value=issue_comment_new_value) + comment_mentions = comment_mentions + new_comment_mentions + + comment_mention_subscribers = extract_mentions_as_subscribers( project_id=project_id, issue_id=issue_id, mentions=all_comment_mentions) + """ + We will not send subscription activity notification to the below mentioned user sets + - Those who have been newly mentioned in the issue description, we will send mention notification to them. + - When the activity is a comment_created and there exist a mention in the comment, then we have to send the "mention_in_comment" notification + - When the activity is a comment_updated and there exist a mention change, then also we have to send the "mention_in_comment" notification + """ + issue_assignees = list( IssueAssignee.objects.filter( project_id=project_id, issue_id=issue_id) - .exclude(assignee_id=actor_id) + .exclude(assignee_id__in=list(new_mentions + comment_mentions)) .values_list("assignee", flat=True) ) - - issue_subscribers = issue_subscribers + issue_assignees + + issue_subscribers = list( + IssueSubscriber.objects.filter( + project_id=project_id, issue_id=issue_id) + .exclude(subscriber_id__in=list(new_mentions + comment_mentions + [actor_id])) + .values_list("subscriber", flat=True) + ) issue = Issue.objects.filter(pk=issue_id).first() + if (issue.created_by_id is not None and str(issue.created_by_id) != str(actor_id)): + issue_subscribers = issue_subscribers + [issue.created_by_id] + if subscriber: # add the user to issue subscriber try: - _ = IssueSubscriber.objects.get_or_create( - issue_id=issue_id, subscriber_id=actor_id - ) + if str(issue.created_by_id) != str(actor_id) and uuid.UUID(actor_id) not in issue_assignees: + _ = IssueSubscriber.objects.get_or_create( + project_id=project_id, issue_id=issue_id, subscriber_id=actor_id + ) except Exception as e: pass project = Project.objects.get(pk=project_id) - for subscriber in list(set(issue_subscribers)): + issue_subscribers = list(set(issue_subscribers + issue_assignees) - {uuid.UUID(actor_id)}) + + for subscriber in issue_subscribers: + if subscriber in issue_subscribers: + sender = "in_app:issue_activities:subscribed" + if issue.created_by_id is not None and subscriber == issue.created_by_id: + sender = "in_app:issue_activities:created" + if subscriber in issue_assignees: + sender = "in_app:issue_activities:assigned" + for issue_activity in issue_activities_created: issue_comment = issue_activity.get("issue_comment") if issue_comment is not None: - issue_comment = IssueComment.objects.get(id=issue_comment, issue_id=issue_id, project_id=project_id, workspace_id=project.workspace_id) + issue_comment = IssueComment.objects.get( + id=issue_comment, issue_id=issue_id, project_id=project_id, workspace_id=project.workspace_id) + bulk_notifications.append( Notification( workspace=project.workspace, - sender="in_app:issue_activities", + sender=sender, triggered_by_id=actor_id, receiver_id=subscriber, entity_identifier=issue_id, @@ -215,15 +333,42 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi # Add Mentioned as Issue Subscribers IssueSubscriber.objects.bulk_create( - mention_subscribers, batch_size=100) + mention_subscribers + comment_mention_subscribers, batch_size=100) - for mention_id in new_mentions: + last_activity = ( + IssueActivity.objects.filter(issue_id=issue_id) + .order_by("-created_at") + .first() + ) + + actor = User.objects.get(pk=actor_id) + + for mention_id in comment_mentions: if (mention_id != actor_id): for issue_activity in issue_activities_created: + notification = createMentionNotification( + project=project, + issue=issue, + notification_comment=f"{actor.display_name} has mentioned you in a comment in issue {issue.name}", + actor_id=actor_id, + mention_id=mention_id, + issue_id=issue_id, + activity=issue_activity + ) + bulk_notifications.append(notification) + + + for mention_id in new_mentions: + if (mention_id != actor_id): + if ( + last_activity is not None + and last_activity.field == "description" + and actor_id == str(last_activity.actor_id) + ): bulk_notifications.append( - Notification( - workspace=project.workspace, - sender="in_app:issue_activities:mention", + Notification( + workspace=project.workspace, + sender="in_app:issue_activities:mentioned", triggered_by_id=actor_id, receiver_id=mention_id, entity_identifier=issue_id, @@ -237,38 +382,37 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi "identifier": str(issue.project.identifier), "sequence_id": issue.sequence_id, "state_name": issue.state.name, - "state_group": issue.state.group, - }, - "issue_activity": { - "id": str(issue_activity.get("id")), - "verb": str(issue_activity.get("verb")), - "field": str(issue_activity.get("field")), - "actor": str(issue_activity.get("actor_id")), - "new_value": str(issue_activity.get("new_value")), - "old_value": str(issue_activity.get("old_value")), - }, - }, + "state_group": issue.state.group, + }, + "issue_activity": { + "id": str(last_activity.id), + "verb": str(last_activity.verb), + "field": str(last_activity.field), + "actor": str(last_activity.actor_id), + "new_value": str(last_activity.new_value), + "old_value": str(last_activity.old_value), + }, + }, + ) + ) + else: + for issue_activity in issue_activities_created: + notification = createMentionNotification( + project=project, + issue=issue, + notification_comment=f"You have been mentioned in the issue {issue.name}", + actor_id=actor_id, + mention_id=mention_id, + issue_id=issue_id, + activity=issue_activity ) - ) - - # Create New Mentions Here - aggregated_issue_mentions = [] - - for mention_id in new_mentions: - mentioned_user = User.objects.get(pk=mention_id) - aggregated_issue_mentions.append( - IssueMention( - mention=mentioned_user, - issue=issue, - project=project, - workspace=project.workspace - ) - ) - - IssueMention.objects.bulk_create( - aggregated_issue_mentions, batch_size=100) - IssueMention.objects.filter( - issue=issue, mention__in=removed_mention).delete() + bulk_notifications.append(notification) + # save new mentions for the particular issue and remove the mentions that has been deleted from the description + update_mentions_for_issue(issue=issue, project=project, new_mentions=new_mentions, + removed_mention=removed_mention) + # Bulk create notifications Notification.objects.bulk_create(bulk_notifications, batch_size=100) + + diff --git a/apiserver/plane/utils/integrations/slack.py b/apiserver/plane/utils/integrations/slack.py new file mode 100644 index 00000000000..70f26e16091 --- /dev/null +++ b/apiserver/plane/utils/integrations/slack.py @@ -0,0 +1,20 @@ +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/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index 33a0f6673ce..c571291cfac 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -10,6 +10,8 @@ x-app-env : &app-env - SENTRY_DSN=${SENTRY_DSN:-""} - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""} - DOCKERIZED=${DOCKERIZED:-1} + # Gunicorn Workers + - GUNICORN_WORKERS=${GUNICORN_WORKERS:-2} #DB SETTINGS - PGHOST=${PGHOST:-plane-db} - PGDATABASE=${PGDATABASE:-plane} diff --git a/deploy/selfhost/variables.env b/deploy/selfhost/variables.env index 1e507a54b6b..b12031126ce 100644 --- a/deploy/selfhost/variables.env +++ b/deploy/selfhost/variables.env @@ -61,3 +61,5 @@ MINIO_ROOT_PASSWORD="secret-key" BUCKET_NAME=uploads FILE_SIZE_LIMIT=5242880 +# Gunicorn Workers +GUNICORN_WORKERS=2 diff --git a/packages/editor/core/Readme.md b/packages/editor/core/Readme.md index 56d1a502c79..aafda700866 100644 --- a/packages/editor/core/Readme.md +++ b/packages/editor/core/Readme.md @@ -19,27 +19,27 @@ This allows for extensive customization and flexibility in the Editors created u 1. useEditor - A hook that you can use to extend the Plane editor. - | Prop | Type | Description | - | --- | --- | --- | - | `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features | - | `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object | - | `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | - | `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | - | `value` | `html string` | The initial content of the editor. | - | `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | - | `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | - | `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | - | `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert in case of content not being "saved". | - | `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component | + | Prop | Type | Description | + | ------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features | + | `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object | + | `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | + | `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | + | `value` | `html string` | The initial content of the editor. | + | `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | + | `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | + | `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | + | `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert in case of content not being "saved". | + | `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component | 2. useReadOnlyEditor - A hook that can be used to extend a Read Only instance of the core editor. - | Prop | Type | Description | - | --- | --- | --- | - | `value` | `string` | The initial content of the editor. | - | `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component | - | `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features | - | `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object | + | Prop | Type | Description | + | -------------- | ------------- | ------------------------------------------------------------------------------------------ | + | `value` | `string` | The initial content of the editor. | + | `forwardedRef` | `any` | Pass this in whenever you want to control the editor's state from an external component | + | `extensions` | `Extension[]` | An array of custom extensions you want to add into the editor to extend it's core features | + | `editorProps` | `EditorProps` | Extend the editor props by passing in a custom props object | 3. Items and Commands - H1, H2, H3, task list, quote, code block, etc's methods. @@ -51,7 +51,11 @@ This allows for extensive customization and flexibility in the Editors created u 5. Extending with Custom Styles ```ts -const customEditorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); +const customEditorClassNames = getEditorClassNames({ + noBorder, + borderOnFocus, + customClassName, +}); ``` ## Core features diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index 83f59a1f4a9..ab6c77724e5 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -2,6 +2,7 @@ "name": "@plane/editor-core", "version": "0.0.1", "description": "Core Editor that powers Plane", + "private": true, "main": "./dist/index.mjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index 497a63ca61a..8f9e3635079 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -3,18 +3,36 @@ import { UploadImage } from "../types/upload-image"; import { startImageUpload } from "../ui/plugins/upload-image"; export const toggleHeadingOne = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); - else editor.chain().focus().toggleHeading({ level: 1 }).run() + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .setNode("heading", { level: 1 }) + .run(); + else editor.chain().focus().toggleHeading({ level: 1 }).run(); }; export const toggleHeadingTwo = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); - else editor.chain().focus().toggleHeading({ level: 2 }).run() + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .setNode("heading", { level: 2 }) + .run(); + else editor.chain().focus().toggleHeading({ level: 2 }).run(); }; export const toggleHeadingThree = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); - else editor.chain().focus().toggleHeading({ level: 3 }).run() + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .setNode("heading", { level: 3 }) + .run(); + else editor.chain().focus().toggleHeading({ level: 3 }).run(); }; export const toggleBold = (editor: Editor, range?: Range) => { @@ -37,7 +55,8 @@ export const toggleCode = (editor: Editor, range?: Range) => { else editor.chain().focus().toggleCode().run(); }; export const toggleOrderedList = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleOrderedList().run(); + if (range) + editor.chain().focus().deleteRange(range).toggleOrderedList().run(); else editor.chain().focus().toggleOrderedList().run(); }; @@ -48,7 +67,7 @@ export const toggleBulletList = (editor: Editor, range?: Range) => { export const toggleTaskList = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).toggleTaskList().run(); - else editor.chain().focus().toggleTaskList().run() + else editor.chain().focus().toggleTaskList().run(); }; export const toggleStrike = (editor: Editor, range?: Range) => { @@ -57,13 +76,37 @@ export const toggleStrike = (editor: Editor, range?: Range) => { }; export const toggleBlockquote = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(); - else editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(); + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .toggleNode("paragraph", "paragraph") + .toggleBlockquote() + .run(); + else + editor + .chain() + .focus() + .toggleNode("paragraph", "paragraph") + .toggleBlockquote() + .run(); }; export const insertTableCommand = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); - else editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); + if (range) + editor + .chain() + .focus() + .deleteRange(range) + .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) + .run(); + else + editor + .chain() + .focus() + .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) + .run(); }; export const unsetLinkEditor = (editor: Editor) => { @@ -74,7 +117,14 @@ export const setLinkEditor = (editor: Editor, url: string) => { editor.chain().focus().setLink({ href: url }).run(); }; -export const insertImageCommand = (editor: Editor, uploadFile: UploadImage, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void, range?: Range) => { +export const insertImageCommand = ( + editor: Editor, + uploadFile: UploadImage, + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, + range?: Range, +) => { if (range) editor.chain().focus().deleteRange(range).run(); const input = document.createElement("input"); input.type = "file"; @@ -88,4 +138,3 @@ export const insertImageCommand = (editor: Editor, uploadFile: UploadImage, setI }; input.click(); }; - diff --git a/packages/editor/core/src/lib/utils.ts b/packages/editor/core/src/lib/utils.ts index 48467478022..f426b70b74b 100644 --- a/packages/editor/core/src/lib/utils.ts +++ b/packages/editor/core/src/lib/utils.ts @@ -6,19 +6,24 @@ interface EditorClassNames { customClassName?: string; } -export const getEditorClassNames = ({ noBorder, borderOnFocus, customClassName }: EditorClassNames) => cn( - 'relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md', - noBorder ? '' : 'border border-custom-border-200', - borderOnFocus ? 'focus:border border-custom-border-300' : 'focus:border-0', - customClassName -); +export const getEditorClassNames = ({ + noBorder, + borderOnFocus, + customClassName, +}: EditorClassNames) => + cn( + "relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md", + noBorder ? "" : "border border-custom-border-200", + borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0", + customClassName, + ); export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export const findTableAncestor = ( - node: Node | null + node: Node | null, ): HTMLTableElement | null => { while (node !== null && node.nodeName !== "TABLE") { node = node.parentNode; @@ -27,10 +32,10 @@ export const findTableAncestor = ( }; export const getTrimmedHTML = (html: string) => { - html = html.replace(/^(

<\/p>)+/, ''); - html = html.replace(/(

<\/p>)+$/, ''); + html = html.replace(/^(

<\/p>)+/, ""); + html = html.replace(/(

<\/p>)+$/, ""); return html; -} +}; export const isValidHttpUrl = (string: string): boolean => { let url: URL; @@ -42,4 +47,4 @@ export const isValidHttpUrl = (string: string): boolean => { } return url.protocol === "http:" || url.protocol === "https:"; -} +}; diff --git a/packages/editor/core/src/types/mention-suggestion.ts b/packages/editor/core/src/types/mention-suggestion.ts index 9c9ab76069c..dcaa3148d63 100644 --- a/packages/editor/core/src/types/mention-suggestion.ts +++ b/packages/editor/core/src/types/mention-suggestion.ts @@ -1,10 +1,10 @@ export type IMentionSuggestion = { - id: string; - type: string; - avatar: string; - title: string; - subtitle: string; - redirect_uri: string; -} + id: string; + type: string; + avatar: string; + title: string; + subtitle: string; + redirect_uri: string; +}; -export type IMentionHighlight = string \ No newline at end of file +export type IMentionHighlight = string; diff --git a/packages/editor/core/src/ui/components/editor-content.tsx b/packages/editor/core/src/ui/components/editor-content.tsx index d0531da0181..830b87d9cfd 100644 --- a/packages/editor/core/src/ui/components/editor-content.tsx +++ b/packages/editor/core/src/ui/components/editor-content.tsx @@ -8,10 +8,16 @@ interface EditorContentProps { children?: ReactNode; } -export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = '', children }: EditorContentProps) => ( +export const EditorContentWrapper = ({ + editor, + editorContentCustomClassNames = "", + children, +}: EditorContentProps) => (

- {(editor?.isActive("image") && editor?.isEditable) && } + {editor?.isActive("image") && editor?.isEditable && ( + + )} {children}
); diff --git a/packages/editor/core/src/ui/extensions/image/image-resize.tsx b/packages/editor/core/src/ui/extensions/image/image-resize.tsx index 5e86475cf2c..2545c7e441f 100644 --- a/packages/editor/core/src/ui/extensions/image/image-resize.tsx +++ b/packages/editor/core/src/ui/extensions/image/image-resize.tsx @@ -3,7 +3,9 @@ import Moveable from "react-moveable"; export const ImageResizer = ({ editor }: { editor: Editor }) => { const updateMediaSize = () => { - const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement; + const imageInfo = document.querySelector( + ".ProseMirror-selectednode", + ) as HTMLImageElement; if (imageInfo) { const selection = editor.state.selection; editor.commands.setImage({ diff --git a/packages/editor/core/src/ui/extensions/image/index.tsx b/packages/editor/core/src/ui/extensions/image/index.tsx index f9345509d98..aea84c6b884 100644 --- a/packages/editor/core/src/ui/extensions/image/index.tsx +++ b/packages/editor/core/src/ui/extensions/image/index.tsx @@ -3,21 +3,28 @@ import TrackImageDeletionPlugin from "../../plugins/delete-image"; import UploadImagesPlugin from "../../plugins/upload-image"; import { DeleteImage } from "../../../types/delete-image"; -const ImageExtension = (deleteImage: DeleteImage) => Image.extend({ - addProseMirrorPlugins() { - return [UploadImagesPlugin(), TrackImageDeletionPlugin(deleteImage)]; - }, - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - height: { - default: null, - }, - }; - }, -}); +const ImageExtension = ( + deleteImage: DeleteImage, + cancelUploadImage?: () => any, +) => + Image.extend({ + addProseMirrorPlugins() { + return [ + UploadImagesPlugin(cancelUploadImage), + TrackImageDeletionPlugin(deleteImage), + ]; + }, + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + height: { + default: null, + }, + }; + }, + }); export default ImageExtension; diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index a7621ab20cb..3f191a9127c 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -20,82 +20,89 @@ import { isValidHttpUrl } from "../../lib/utils"; import { IMentionSuggestion } from "../../types/mention-suggestion"; import { Mentions } from "../mentions"; - export const CoreEditorExtensions = ( - mentionConfig: { mentionSuggestions: IMentionSuggestion[], mentionHighlights: string[] }, + mentionConfig: { + mentionSuggestions: IMentionSuggestion[]; + mentionHighlights: string[]; + }, deleteFile: DeleteImage, + cancelUploadImage?: () => any, ) => [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", - }, - }, - orderedList: { - HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", - }, - }, - listItem: { - HTMLAttributes: { - class: "leading-normal -mb-2", - }, - }, - blockquote: { - HTMLAttributes: { - class: "border-l-4 border-custom-border-300", - }, - }, - code: { - HTMLAttributes: { - class: - "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", - spellcheck: "false", - }, - }, - codeBlock: false, - horizontalRule: false, - dropcursor: { - color: "rgba(var(--color-text-100))", - width: 2, + StarterKit.configure({ + bulletList: { + HTMLAttributes: { + class: "list-disc list-outside leading-3 -mt-2", }, - gapcursor: false, - }), - Gapcursor, - TiptapLink.configure({ - protocols: ["http", "https"], - validate: (url) => isValidHttpUrl(url), + }, + orderedList: { HTMLAttributes: { - class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", + class: "list-decimal list-outside leading-3 -mt-2", }, - }), - ImageExtension(deleteFile).configure({ + }, + listItem: { HTMLAttributes: { - class: "rounded-lg border border-custom-border-300", + class: "leading-normal -mb-2", }, - }), - TiptapUnderline, - TextStyle, - Color, - TaskList.configure({ + }, + blockquote: { HTMLAttributes: { - class: "not-prose pl-2", + class: "border-l-4 border-custom-border-300", }, - }), - TaskItem.configure({ + }, + code: { HTMLAttributes: { - class: "flex items-start my-4", + class: + "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", + spellcheck: "false", }, - nested: true, - }), - Markdown.configure({ - html: true, - transformCopiedText: true, - }), - Table, - TableHeader, - TableCell, - TableRow, - Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false), - ]; + }, + codeBlock: false, + horizontalRule: false, + dropcursor: { + color: "rgba(var(--color-text-100))", + width: 2, + }, + gapcursor: false, + }), + Gapcursor, + TiptapLink.configure({ + protocols: ["http", "https"], + validate: (url) => isValidHttpUrl(url), + HTMLAttributes: { + class: + "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", + }, + }), + ImageExtension(deleteFile, cancelUploadImage).configure({ + HTMLAttributes: { + class: "rounded-lg border border-custom-border-300", + }, + }), + TiptapUnderline, + TextStyle, + Color, + TaskList.configure({ + HTMLAttributes: { + class: "not-prose pl-2", + }, + }), + TaskItem.configure({ + HTMLAttributes: { + class: "flex items-start my-4", + }, + nested: true, + }), + Markdown.configure({ + html: true, + transformCopiedText: true, + }), + Table, + TableHeader, + TableCell, + TableRow, + Mentions( + mentionConfig.mentionSuggestions, + mentionConfig.mentionHighlights, + false, + ), +]; diff --git a/packages/editor/core/src/ui/extensions/table/table-cell/index.ts b/packages/editor/core/src/ui/extensions/table/table-cell/index.ts index b39fe7104e5..fb21833810d 100644 --- a/packages/editor/core/src/ui/extensions/table/table-cell/index.ts +++ b/packages/editor/core/src/ui/extensions/table/table-cell/index.ts @@ -1 +1 @@ -export { default as default } from "./table-cell" +export { default as default } from "./table-cell"; diff --git a/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts index ac43875dac8..1d3e57af96e 100644 --- a/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts +++ b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts @@ -1,7 +1,7 @@ -import { mergeAttributes, Node } from "@tiptap/core" +import { mergeAttributes, Node } from "@tiptap/core"; export interface TableCellOptions { - HTMLAttributes: Record + HTMLAttributes: Record; } export default Node.create({ @@ -9,8 +9,8 @@ export default Node.create({ addOptions() { return { - HTMLAttributes: {} - } + HTMLAttributes: {}, + }; }, content: "paragraph+", @@ -18,24 +18,24 @@ export default Node.create({ addAttributes() { return { colspan: { - default: 1 + default: 1, }, rowspan: { - default: 1 + default: 1, }, colwidth: { default: null, parseHTML: (element) => { - const colwidth = element.getAttribute("colwidth") - const value = colwidth ? [parseInt(colwidth, 10)] : null + const colwidth = element.getAttribute("colwidth"); + const value = colwidth ? [parseInt(colwidth, 10)] : null; - return value - } + return value; + }, }, background: { - default: "none" - } - } + default: "none", + }, + }; }, tableRole: "cell", @@ -43,16 +43,16 @@ export default Node.create({ isolating: true, parseHTML() { - return [{ tag: "td" }] + return [{ tag: "td" }]; }, renderHTML({ node, HTMLAttributes }) { return [ "td", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - style: `background-color: ${node.attrs.background}` + style: `background-color: ${node.attrs.background}`, }), - 0 - ] - } -}) + 0, + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/table/table-header/index.ts b/packages/editor/core/src/ui/extensions/table/table-header/index.ts index 57137dedd43..cb036c50545 100644 --- a/packages/editor/core/src/ui/extensions/table/table-header/index.ts +++ b/packages/editor/core/src/ui/extensions/table/table-header/index.ts @@ -1 +1 @@ -export { default as default } from "./table-header" +export { default as default } from "./table-header"; diff --git a/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts index 712ca65f073..0148f1a6f75 100644 --- a/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts +++ b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts @@ -1,15 +1,15 @@ -import { mergeAttributes, Node } from "@tiptap/core" +import { mergeAttributes, Node } from "@tiptap/core"; export interface TableHeaderOptions { - HTMLAttributes: Record + HTMLAttributes: Record; } export default Node.create({ name: "tableHeader", addOptions() { return { - HTMLAttributes: {} - } + HTMLAttributes: {}, + }; }, content: "paragraph+", @@ -17,24 +17,24 @@ export default Node.create({ addAttributes() { return { colspan: { - default: 1 + default: 1, }, rowspan: { - default: 1 + default: 1, }, colwidth: { default: null, parseHTML: (element) => { - const colwidth = element.getAttribute("colwidth") - const value = colwidth ? [parseInt(colwidth, 10)] : null + const colwidth = element.getAttribute("colwidth"); + const value = colwidth ? [parseInt(colwidth, 10)] : null; - return value - } + return value; + }, }, background: { - default: "rgb(var(--color-primary-100))" - } - } + default: "rgb(var(--color-primary-100))", + }, + }; }, tableRole: "header_cell", @@ -42,16 +42,16 @@ export default Node.create({ isolating: true, parseHTML() { - return [{ tag: "th" }] + return [{ tag: "th" }]; }, renderHTML({ node, HTMLAttributes }) { return [ "th", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - style: `background-color: ${node.attrs.background}` + style: `background-color: ${node.attrs.background}`, }), - 0 - ] - } -}) + 0, + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/table/table-row/index.ts b/packages/editor/core/src/ui/extensions/table/table-row/index.ts index 9ecc2c0ae57..8c6eb55aaf9 100644 --- a/packages/editor/core/src/ui/extensions/table/table-row/index.ts +++ b/packages/editor/core/src/ui/extensions/table/table-row/index.ts @@ -1 +1 @@ -export { default as default } from "./table-row" +export { default as default } from "./table-row"; diff --git a/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts index e922e7fa197..1b576623b1c 100644 --- a/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts +++ b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts @@ -1,31 +1,31 @@ -import { mergeAttributes, Node } from "@tiptap/core" +import { mergeAttributes, Node } from "@tiptap/core"; export interface TableRowOptions { - HTMLAttributes: Record + HTMLAttributes: Record; } export default Node.create({ - name: "tableRow", + name: "tableRow", - addOptions() { - return { - HTMLAttributes: {} - } - }, + addOptions() { + return { + HTMLAttributes: {}, + }; + }, - content: "(tableCell | tableHeader)*", + content: "(tableCell | tableHeader)*", - tableRole: "row", + tableRole: "row", - parseHTML() { - return [{ tag: "tr" }] - }, + parseHTML() { + return [{ tag: "tr" }]; + }, - renderHTML({ HTMLAttributes }) { - return [ - "tr", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), - 0 - ] - } -}) + renderHTML({ HTMLAttributes }) { + return [ + "tr", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + 0, + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/table/table/icons.ts b/packages/editor/core/src/ui/extensions/table/table/icons.ts index d3159d4aa8c..eda520759c7 100644 --- a/packages/editor/core/src/ui/extensions/table/table/icons.ts +++ b/packages/editor/core/src/ui/extensions/table/table/icons.ts @@ -38,7 +38,7 @@ const icons = { /> `, - insertBottomTableIcon:` { onSelectColor(value); colorPicker.hide(); @@ -331,7 +333,9 @@ export class TableView implements NodeView { this.rowsControl = h( "div", { className: "rowsControl" }, - h("button", { + h("div", { + itemType: "button", + className: "rowsControlDiv", onClick: () => this.selectRow(), }), ); @@ -339,7 +343,9 @@ export class TableView implements NodeView { this.columnsControl = h( "div", { className: "columnsControl" }, - h("button", { + h("div", { + itemType: "button", + className: "columnsControlDiv", onClick: () => this.selectColumn(), }), ); @@ -352,7 +358,7 @@ export class TableView implements NodeView { ); this.columnsToolbox = createToolbox({ - triggerButton: this.columnsControl.querySelector("button"), + triggerButton: this.columnsControl.querySelector(".columnsControlDiv"), items: columnsToolboxItems, tippyOptions: { ...defaultTippyOptions, diff --git a/packages/editor/core/src/ui/extensions/table/table/table.ts b/packages/editor/core/src/ui/extensions/table/table/table.ts index eab3cad92e0..8571fdfba85 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table.ts +++ b/packages/editor/core/src/ui/extensions/table/table/table.ts @@ -1,298 +1,312 @@ -import { TextSelection } from "@tiptap/pm/state" +import { TextSelection } from "@tiptap/pm/state"; -import { callOrReturn, getExtensionField, mergeAttributes, Node, ParentConfig } from "@tiptap/core" import { - addColumnAfter, - addColumnBefore, - addRowAfter, - addRowBefore, - CellSelection, - columnResizing, - deleteColumn, - deleteRow, - deleteTable, - fixTables, - goToNextCell, - mergeCells, - setCellAttr, - splitCell, - tableEditing, - toggleHeader, - toggleHeaderCell -} from "@tiptap/prosemirror-tables" - -import { tableControls } from "./table-controls" -import { TableView } from "./table-view" -import { createTable } from "./utilities/create-table" -import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected" + callOrReturn, + getExtensionField, + mergeAttributes, + Node, + ParentConfig, +} from "@tiptap/core"; +import { + addColumnAfter, + addColumnBefore, + addRowAfter, + addRowBefore, + CellSelection, + columnResizing, + deleteColumn, + deleteRow, + deleteTable, + fixTables, + goToNextCell, + mergeCells, + setCellAttr, + splitCell, + tableEditing, + toggleHeader, + toggleHeaderCell, +} from "@tiptap/prosemirror-tables"; + +import { tableControls } from "./table-controls"; +import { TableView } from "./table-view"; +import { createTable } from "./utilities/create-table"; +import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected"; export interface TableOptions { - HTMLAttributes: Record - resizable: boolean - handleWidth: number - cellMinWidth: number - lastColumnResizable: boolean - allowTableNodeSelection: boolean + HTMLAttributes: Record; + resizable: boolean; + handleWidth: number; + cellMinWidth: number; + lastColumnResizable: boolean; + allowTableNodeSelection: boolean; } declare module "@tiptap/core" { - interface Commands { - table: { - insertTable: (options?: { - rows?: number - cols?: number - withHeaderRow?: boolean - }) => ReturnType - addColumnBefore: () => ReturnType - addColumnAfter: () => ReturnType - deleteColumn: () => ReturnType - addRowBefore: () => ReturnType - addRowAfter: () => ReturnType - deleteRow: () => ReturnType - deleteTable: () => ReturnType - mergeCells: () => ReturnType - splitCell: () => ReturnType - toggleHeaderColumn: () => ReturnType - toggleHeaderRow: () => ReturnType - toggleHeaderCell: () => ReturnType - mergeOrSplit: () => ReturnType - setCellAttribute: (name: string, value: any) => ReturnType - goToNextCell: () => ReturnType - goToPreviousCell: () => ReturnType - fixTables: () => ReturnType - setCellSelection: (position: { - anchorCell: number - headCell?: number - }) => ReturnType - } - } - - interface NodeConfig { - tableRole?: - | string - | ((this: { - name: string - options: Options - storage: Storage - parent: ParentConfig>["tableRole"] - }) => string) - } + interface Commands { + table: { + insertTable: (options?: { + rows?: number; + cols?: number; + withHeaderRow?: boolean; + }) => ReturnType; + addColumnBefore: () => ReturnType; + addColumnAfter: () => ReturnType; + deleteColumn: () => ReturnType; + addRowBefore: () => ReturnType; + addRowAfter: () => ReturnType; + deleteRow: () => ReturnType; + deleteTable: () => ReturnType; + mergeCells: () => ReturnType; + splitCell: () => ReturnType; + toggleHeaderColumn: () => ReturnType; + toggleHeaderRow: () => ReturnType; + toggleHeaderCell: () => ReturnType; + mergeOrSplit: () => ReturnType; + setCellAttribute: (name: string, value: any) => ReturnType; + goToNextCell: () => ReturnType; + goToPreviousCell: () => ReturnType; + fixTables: () => ReturnType; + setCellSelection: (position: { + anchorCell: number; + headCell?: number; + }) => ReturnType; + }; + } + + interface NodeConfig { + tableRole?: + | string + | ((this: { + name: string; + options: Options; + storage: Storage; + parent: ParentConfig>["tableRole"]; + }) => string); + } } export default Node.create({ - name: "table", - - addOptions() { - return { - HTMLAttributes: {}, - resizable: true, - handleWidth: 5, - cellMinWidth: 100, - lastColumnResizable: true, - allowTableNodeSelection: true - } - }, - - content: "tableRow+", - - tableRole: "table", - - isolating: true, - - group: "block", - - allowGapCursor: false, - - parseHTML() { - return [{ tag: "table" }] - }, - - renderHTML({ HTMLAttributes }) { - return [ - "table", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), - ["tbody", 0] - ] - }, - - addCommands() { - return { - insertTable: - ({ rows = 3, cols = 3, withHeaderRow = true} = {}) => - ({ tr, dispatch, editor }) => { - const node = createTable( - editor.schema, - rows, - cols, - withHeaderRow - ) - - if (dispatch) { - const offset = tr.selection.anchor + 1 - - tr.replaceSelectionWith(node) - .scrollIntoView() - .setSelection( - TextSelection.near(tr.doc.resolve(offset)) - ) - } - - return true - }, - addColumnBefore: - () => - ({ state, dispatch }) => addColumnBefore(state, dispatch), - addColumnAfter: - () => - ({ state, dispatch }) => addColumnAfter(state, dispatch), - deleteColumn: - () => - ({ state, dispatch }) => deleteColumn(state, dispatch), - addRowBefore: - () => - ({ state, dispatch }) => addRowBefore(state, dispatch), - addRowAfter: - () => - ({ state, dispatch }) => addRowAfter(state, dispatch), - deleteRow: - () => - ({ state, dispatch }) => deleteRow(state, dispatch), - deleteTable: - () => - ({ state, dispatch }) => deleteTable(state, dispatch), - mergeCells: - () => - ({ state, dispatch }) => mergeCells(state, dispatch), - splitCell: - () => - ({ state, dispatch }) => splitCell(state, dispatch), - toggleHeaderColumn: - () => - ({ state, dispatch }) => toggleHeader("column")(state, dispatch), - toggleHeaderRow: - () => - ({ state, dispatch }) => toggleHeader("row")(state, dispatch), - toggleHeaderCell: - () => - ({ state, dispatch }) => toggleHeaderCell(state, dispatch), - mergeOrSplit: - () => - ({ state, dispatch }) => { - if (mergeCells(state, dispatch)) { - return true - } - - return splitCell(state, dispatch) - }, - setCellAttribute: - (name, value) => - ({ state, dispatch }) => setCellAttr(name, value)(state, dispatch), - goToNextCell: - () => - ({ state, dispatch }) => goToNextCell(1)(state, dispatch), - goToPreviousCell: - () => - ({ state, dispatch }) => goToNextCell(-1)(state, dispatch), - fixTables: - () => - ({ state, dispatch }) => { - if (dispatch) { - fixTables(state) - } - - return true - }, - setCellSelection: - (position) => - ({ tr, dispatch }) => { - if (dispatch) { - const selection = CellSelection.create( - tr.doc, - position.anchorCell, - position.headCell - ) - - // @ts-ignore - tr.setSelection(selection) - } - - return true - } - } - }, - - addKeyboardShortcuts() { - return { - Tab: () => { - if (this.editor.commands.goToNextCell()) { - return true - } - - if (!this.editor.can().addRowAfter()) { - return false - } - - return this.editor.chain().addRowAfter().goToNextCell().run() - }, - "Shift-Tab": () => this.editor.commands.goToPreviousCell(), - Backspace: deleteTableWhenAllCellsSelected, - "Mod-Backspace": deleteTableWhenAllCellsSelected, - Delete: deleteTableWhenAllCellsSelected, - "Mod-Delete": deleteTableWhenAllCellsSelected - } - }, - - addNodeView() { - return ({ editor, getPos, node, decorations }) => { - const { cellMinWidth } = this.options - - return new TableView( - node, - cellMinWidth, - decorations, - editor, - getPos as () => number - ) - } - }, - - addProseMirrorPlugins() { - const isResizable = this.options.resizable && this.editor.isEditable - - const plugins = [ - tableEditing({ - allowTableNodeSelection: this.options.allowTableNodeSelection - }), - tableControls() - ] - - if (isResizable) { - plugins.unshift( - columnResizing({ - handleWidth: this.options.handleWidth, - cellMinWidth: this.options.cellMinWidth, - // View: TableView, - - // @ts-ignore - lastColumnResizable: this.options.lastColumnResizable - }) - ) + name: "table", + + addOptions() { + return { + HTMLAttributes: {}, + resizable: true, + handleWidth: 5, + cellMinWidth: 100, + lastColumnResizable: true, + allowTableNodeSelection: true, + }; + }, + + content: "tableRow+", + + tableRole: "table", + + isolating: true, + + group: "block", + + allowGapCursor: false, + + parseHTML() { + return [{ tag: "table" }]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "table", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + ["tbody", 0], + ]; + }, + + addCommands() { + return { + insertTable: + ({ rows = 3, cols = 3, withHeaderRow = true } = {}) => + ({ tr, dispatch, editor }) => { + const node = createTable(editor.schema, rows, cols, withHeaderRow); + + if (dispatch) { + const offset = tr.selection.anchor + 1; + + tr.replaceSelectionWith(node) + .scrollIntoView() + .setSelection(TextSelection.near(tr.doc.resolve(offset))); + } + + return true; + }, + addColumnBefore: + () => + ({ state, dispatch }) => + addColumnBefore(state, dispatch), + addColumnAfter: + () => + ({ state, dispatch }) => + addColumnAfter(state, dispatch), + deleteColumn: + () => + ({ state, dispatch }) => + deleteColumn(state, dispatch), + addRowBefore: + () => + ({ state, dispatch }) => + addRowBefore(state, dispatch), + addRowAfter: + () => + ({ state, dispatch }) => + addRowAfter(state, dispatch), + deleteRow: + () => + ({ state, dispatch }) => + deleteRow(state, dispatch), + deleteTable: + () => + ({ state, dispatch }) => + deleteTable(state, dispatch), + mergeCells: + () => + ({ state, dispatch }) => + mergeCells(state, dispatch), + splitCell: + () => + ({ state, dispatch }) => + splitCell(state, dispatch), + toggleHeaderColumn: + () => + ({ state, dispatch }) => + toggleHeader("column")(state, dispatch), + toggleHeaderRow: + () => + ({ state, dispatch }) => + toggleHeader("row")(state, dispatch), + toggleHeaderCell: + () => + ({ state, dispatch }) => + toggleHeaderCell(state, dispatch), + mergeOrSplit: + () => + ({ state, dispatch }) => { + if (mergeCells(state, dispatch)) { + return true; + } + + return splitCell(state, dispatch); + }, + setCellAttribute: + (name, value) => + ({ state, dispatch }) => + setCellAttr(name, value)(state, dispatch), + goToNextCell: + () => + ({ state, dispatch }) => + goToNextCell(1)(state, dispatch), + goToPreviousCell: + () => + ({ state, dispatch }) => + goToNextCell(-1)(state, dispatch), + fixTables: + () => + ({ state, dispatch }) => { + if (dispatch) { + fixTables(state); + } + + return true; + }, + setCellSelection: + (position) => + ({ tr, dispatch }) => { + if (dispatch) { + const selection = CellSelection.create( + tr.doc, + position.anchorCell, + position.headCell, + ); + + // @ts-ignore + tr.setSelection(selection); + } + + return true; + }, + }; + }, + + addKeyboardShortcuts() { + return { + Tab: () => { + if (this.editor.commands.goToNextCell()) { + return true; } - return plugins - }, - - extendNodeSchema(extension) { - const context = { - name: extension.name, - options: extension.options, - storage: extension.storage + if (!this.editor.can().addRowAfter()) { + return false; } - return { - tableRole: callOrReturn( - getExtensionField(extension, "tableRole", context) - ) - } + return this.editor.chain().addRowAfter().goToNextCell().run(); + }, + "Shift-Tab": () => this.editor.commands.goToPreviousCell(), + Backspace: deleteTableWhenAllCellsSelected, + "Mod-Backspace": deleteTableWhenAllCellsSelected, + Delete: deleteTableWhenAllCellsSelected, + "Mod-Delete": deleteTableWhenAllCellsSelected, + }; + }, + + addNodeView() { + return ({ editor, getPos, node, decorations }) => { + const { cellMinWidth } = this.options; + + return new TableView( + node, + cellMinWidth, + decorations, + editor, + getPos as () => number, + ); + }; + }, + + addProseMirrorPlugins() { + const isResizable = this.options.resizable && this.editor.isEditable; + + const plugins = [ + tableEditing({ + allowTableNodeSelection: this.options.allowTableNodeSelection, + }), + tableControls(), + ]; + + if (isResizable) { + plugins.unshift( + columnResizing({ + handleWidth: this.options.handleWidth, + cellMinWidth: this.options.cellMinWidth, + // View: TableView, + + // @ts-ignore + lastColumnResizable: this.options.lastColumnResizable, + }), + ); } -}) + + return plugins; + }, + + extendNodeSchema(extension) { + const context = { + name: extension.name, + options: extension.options, + storage: extension.storage, + }; + + return { + tableRole: callOrReturn( + getExtensionField(extension, "tableRole", context), + ), + }; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts index a3d7f2da814..7811341e066 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts @@ -1,12 +1,12 @@ -import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model" +import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model"; export function createCell( - cellType: NodeType, - cellContent?: Fragment | ProsemirrorNode | Array + cellType: NodeType, + cellContent?: Fragment | ProsemirrorNode | Array, ): ProsemirrorNode | null | undefined { - if (cellContent) { - return cellType.createChecked(null, cellContent) - } + if (cellContent) { + return cellType.createChecked(null, cellContent); + } - return cellType.createAndFill() + return cellType.createAndFill(); } diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts index 75bf7cb41db..5805ecf869a 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts @@ -1,45 +1,45 @@ -import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model" +import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model"; -import { createCell } from "./create-cell" -import { getTableNodeTypes } from "./get-table-node-types" +import { createCell } from "./create-cell"; +import { getTableNodeTypes } from "./get-table-node-types"; export function createTable( - schema: Schema, - rowsCount: number, - colsCount: number, - withHeaderRow: boolean, - cellContent?: Fragment | ProsemirrorNode | Array + schema: Schema, + rowsCount: number, + colsCount: number, + withHeaderRow: boolean, + cellContent?: Fragment | ProsemirrorNode | Array, ): ProsemirrorNode { - const types = getTableNodeTypes(schema) - const headerCells: ProsemirrorNode[] = [] - const cells: ProsemirrorNode[] = [] + const types = getTableNodeTypes(schema); + const headerCells: ProsemirrorNode[] = []; + const cells: ProsemirrorNode[] = []; - for (let index = 0; index < colsCount; index += 1) { - const cell = createCell(types.cell, cellContent) + for (let index = 0; index < colsCount; index += 1) { + const cell = createCell(types.cell, cellContent); - if (cell) { - cells.push(cell) - } + if (cell) { + cells.push(cell); + } - if (withHeaderRow) { - const headerCell = createCell(types.header_cell, cellContent) + if (withHeaderRow) { + const headerCell = createCell(types.header_cell, cellContent); - if (headerCell) { - headerCells.push(headerCell) - } - } + if (headerCell) { + headerCells.push(headerCell); + } } + } - const rows: ProsemirrorNode[] = [] + const rows: ProsemirrorNode[] = []; - for (let index = 0; index < rowsCount; index += 1) { - rows.push( - types.row.createChecked( - null, - withHeaderRow && index === 0 ? headerCells : cells - ) - ) - } + for (let index = 0; index < rowsCount; index += 1) { + rows.push( + types.row.createChecked( + null, + withHeaderRow && index === 0 ? headerCells : cells, + ), + ); + } - return types.table.createChecked(null, rows) + return types.table.createChecked(null, rows); } diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts index dcb20b3239f..7fed537057e 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts @@ -1,39 +1,42 @@ -import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core" +import { + findParentNodeClosestToPos, + KeyboardShortcutCommand, +} from "@tiptap/core"; -import { isCellSelection } from "./is-cell-selection" +import { isCellSelection } from "./is-cell-selection"; export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ - editor + editor, }) => { - const { selection } = editor.state + const { selection } = editor.state; - if (!isCellSelection(selection)) { - return false - } + if (!isCellSelection(selection)) { + return false; + } - let cellCount = 0 - const table = findParentNodeClosestToPos( - selection.ranges[0].$from, - (node) => node.type.name === "table" - ) + let cellCount = 0; + const table = findParentNodeClosestToPos( + selection.ranges[0].$from, + (node) => node.type.name === "table", + ); - table?.node.descendants((node) => { - if (node.type.name === "table") { - return false - } + table?.node.descendants((node) => { + if (node.type.name === "table") { + return false; + } - if (["tableCell", "tableHeader"].includes(node.type.name)) { - cellCount += 1 - } - }) + if (["tableCell", "tableHeader"].includes(node.type.name)) { + cellCount += 1; + } + }); - const allCellsSelected = cellCount === selection.ranges.length + const allCellsSelected = cellCount === selection.ranges.length; - if (!allCellsSelected) { - return false - } + if (!allCellsSelected) { + return false; + } - editor.commands.deleteTable() + editor.commands.deleteTable(); - return true -} + return true; +}; diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts index 293878cb0a4..28c322a1f1f 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts @@ -1,21 +1,21 @@ -import { NodeType, Schema } from "prosemirror-model" +import { NodeType, Schema } from "prosemirror-model"; export function getTableNodeTypes(schema: Schema): { [key: string]: NodeType } { - if (schema.cached.tableNodeTypes) { - return schema.cached.tableNodeTypes - } + if (schema.cached.tableNodeTypes) { + return schema.cached.tableNodeTypes; + } - const roles: { [key: string]: NodeType } = {} + const roles: { [key: string]: NodeType } = {}; - Object.keys(schema.nodes).forEach((type) => { - const nodeType = schema.nodes[type] + Object.keys(schema.nodes).forEach((type) => { + const nodeType = schema.nodes[type]; - if (nodeType.spec.tableRole) { - roles[nodeType.spec.tableRole] = nodeType - } - }) + if (nodeType.spec.tableRole) { + roles[nodeType.spec.tableRole] = nodeType; + } + }); - schema.cached.tableNodeTypes = roles + schema.cached.tableNodeTypes = roles; - return roles + return roles; } diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts index 3c36bf055e2..28917a299b7 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts @@ -1,5 +1,5 @@ -import { CellSelection } from "@tiptap/prosemirror-tables" +import { CellSelection } from "@tiptap/prosemirror-tables"; export function isCellSelection(value: unknown): value is CellSelection { - return value instanceof CellSelection + return value instanceof CellSelection; } diff --git a/packages/editor/core/src/ui/hooks/useEditor.tsx b/packages/editor/core/src/ui/hooks/useEditor.tsx index 9fcf200fb98..258da86523e 100644 --- a/packages/editor/core/src/ui/hooks/useEditor.tsx +++ b/packages/editor/core/src/ui/hooks/useEditor.tsx @@ -29,11 +29,13 @@ interface CustomEditorProps { forwardedRef?: any; mentionHighlights?: string[]; mentionSuggestions?: IMentionSuggestion[]; + cancelUploadImage?: () => any; } export const useEditor = ({ uploadFile, deleteFile, + cancelUploadImage, editorProps = {}, value, extensions = [], @@ -42,7 +44,7 @@ export const useEditor = ({ forwardedRef, setShouldShowAlert, mentionHighlights, - mentionSuggestions + mentionSuggestions, }: CustomEditorProps) => { const editor = useCustomEditor( { @@ -50,7 +52,17 @@ export const useEditor = ({ ...CoreEditorProps(uploadFile, setIsSubmitting), ...editorProps, }, - extensions: [...CoreEditorExtensions({ mentionSuggestions: mentionSuggestions ?? [], mentionHighlights: mentionHighlights ?? []}, deleteFile), ...extensions], + extensions: [ + ...CoreEditorExtensions( + { + mentionSuggestions: mentionSuggestions ?? [], + mentionHighlights: mentionHighlights ?? [], + }, + deleteFile, + cancelUploadImage, + ), + ...extensions, + ], content: typeof value === "string" && value.trim() !== "" ? value : "

", onUpdate: async ({ editor }) => { @@ -82,4 +94,4 @@ export const useEditor = ({ } return editor; -}; \ No newline at end of file +}; diff --git a/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx index 9243c2f4e70..75ebddd3c09 100644 --- a/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx +++ b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx @@ -7,7 +7,7 @@ import { } from "react"; import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions"; import { CoreReadOnlyEditorProps } from "../../ui/read-only/props"; -import { EditorProps } from '@tiptap/pm/view'; +import { EditorProps } from "@tiptap/pm/view"; import { IMentionSuggestion } from "../../types/mention-suggestion"; interface CustomReadOnlyEditorProps { @@ -19,7 +19,14 @@ interface CustomReadOnlyEditorProps { mentionSuggestions?: IMentionSuggestion[]; } -export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editorProps = {}, mentionHighlights, mentionSuggestions}: CustomReadOnlyEditorProps) => { +export const useReadOnlyEditor = ({ + value, + forwardedRef, + extensions = [], + editorProps = {}, + mentionHighlights, + mentionSuggestions, +}: CustomReadOnlyEditorProps) => { const editor = useCustomEditor({ editable: false, content: @@ -28,7 +35,13 @@ export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editor ...CoreReadOnlyEditorProps, ...editorProps, }, - extensions: [...CoreReadOnlyEditorExtensions({ mentionSuggestions: mentionSuggestions ?? [], mentionHighlights: mentionHighlights ?? []}), ...extensions], + extensions: [ + ...CoreReadOnlyEditorExtensions({ + mentionSuggestions: mentionSuggestions ?? [], + mentionHighlights: mentionHighlights ?? [], + }), + ...extensions, + ], }); const hasIntiliazedContent = useRef(false); diff --git a/packages/editor/core/src/ui/mentions/custom.tsx b/packages/editor/core/src/ui/mentions/custom.tsx index c3bfa370310..dc4ab5aad61 100644 --- a/packages/editor/core/src/ui/mentions/custom.tsx +++ b/packages/editor/core/src/ui/mentions/custom.tsx @@ -1,11 +1,11 @@ -import { Mention, MentionOptions } from '@tiptap/extension-mention' -import { mergeAttributes } from '@tiptap/core' -import { ReactNodeViewRenderer } from '@tiptap/react' -import mentionNodeView from './mentionNodeView' -import { IMentionHighlight } from '../../types/mention-suggestion' +import { Mention, MentionOptions } from "@tiptap/extension-mention"; +import { mergeAttributes } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import mentionNodeView from "./mentionNodeView"; +import { IMentionHighlight } from "../../types/mention-suggestion"; export interface CustomMentionOptions extends MentionOptions { - mentionHighlights: IMentionHighlight[] - readonly?: boolean + mentionHighlights: IMentionHighlight[]; + readonly?: boolean; } export const CustomMention = Mention.extend({ @@ -21,35 +21,37 @@ export const CustomMention = Mention.extend({ default: null, }, self: { - default: false + default: false, }, redirect_uri: { - default: "/" - } - } + default: "/", + }, + }; }, addNodeView() { - return ReactNodeViewRenderer(mentionNodeView) + return ReactNodeViewRenderer(mentionNodeView); }, parseHTML() { - return [{ - tag: 'mention-component', - getAttrs: (node: string | HTMLElement) => { - if (typeof node === 'string') { - return null; - } - return { - id: node.getAttribute('data-mention-id') || '', - target: node.getAttribute('data-mention-target') || '', - label: node.innerText.slice(1) || '', - redirect_uri: node.getAttribute('redirect_uri') - } + return [ + { + tag: "mention-component", + getAttrs: (node: string | HTMLElement) => { + if (typeof node === "string") { + return null; + } + return { + id: node.getAttribute("data-mention-id") || "", + target: node.getAttribute("data-mention-target") || "", + label: node.innerText.slice(1) || "", + redirect_uri: node.getAttribute("redirect_uri"), + }; + }, }, - }] + ]; }, renderHTML({ HTMLAttributes }) { - return ['mention-component', mergeAttributes(HTMLAttributes)] + return ["mention-component", mergeAttributes(HTMLAttributes)]; }, -}) +}); diff --git a/packages/editor/core/src/ui/mentions/index.tsx b/packages/editor/core/src/ui/mentions/index.tsx index ba1a9ed0b9d..42ec92554c9 100644 --- a/packages/editor/core/src/ui/mentions/index.tsx +++ b/packages/editor/core/src/ui/mentions/index.tsx @@ -2,14 +2,21 @@ import suggestion from "./suggestion"; import { CustomMention } from "./custom"; -import { IMentionHighlight, IMentionSuggestion } from "../../types/mention-suggestion"; - -export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighlights: IMentionHighlight[], readonly) => CustomMention.configure({ - HTMLAttributes: { - 'class' : "mention", - }, - readonly: readonly, - mentionHighlights: mentionHighlights, - suggestion: suggestion(mentionSuggestions), -}) +import { + IMentionHighlight, + IMentionSuggestion, +} from "../../types/mention-suggestion"; +export const Mentions = ( + mentionSuggestions: IMentionSuggestion[], + mentionHighlights: IMentionHighlight[], + readonly, +) => + CustomMention.configure({ + HTMLAttributes: { + class: "mention", + }, + readonly: readonly, + mentionHighlights: mentionHighlights, + suggestion: suggestion(mentionSuggestions), + }); diff --git a/packages/editor/core/src/ui/mentions/suggestion.ts b/packages/editor/core/src/ui/mentions/suggestion.ts index b4bbc53a6b1..ce09cb0922e 100644 --- a/packages/editor/core/src/ui/mentions/suggestion.ts +++ b/packages/editor/core/src/ui/mentions/suggestion.ts @@ -1,12 +1,17 @@ -import { ReactRenderer } from '@tiptap/react' +import { ReactRenderer } from "@tiptap/react"; import { Editor } from "@tiptap/core"; -import tippy from 'tippy.js' +import tippy from "tippy.js"; -import MentionList from './MentionList' -import { IMentionSuggestion } from '../../types/mention-suggestion'; +import MentionList from "./MentionList"; +import { IMentionSuggestion } from "../../types/mention-suggestion"; const Suggestion = (suggestions: IMentionSuggestion[]) => ({ - items: ({ query }: { query: string }) => suggestions.filter(suggestion => suggestion.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5), + items: ({ query }: { query: string }) => + suggestions + .filter((suggestion) => + suggestion.title.toLowerCase().startsWith(query.toLowerCase()), + ) + .slice(0, 5), render: () => { let reactRenderer: ReactRenderer | null = null; let popup: any | null = null; @@ -30,7 +35,7 @@ const Suggestion = (suggestions: IMentionSuggestion[]) => ({ }, onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => { - reactRenderer?.updateProps(props) + reactRenderer?.updateProps(props); popup && popup[0].setProps({ @@ -49,11 +54,10 @@ const Suggestion = (suggestions: IMentionSuggestion[]) => ({ }, onExit: () => { popup?.[0].destroy(); - reactRenderer?.destroy() + reactRenderer?.destroy(); }, - } + }; }, -}) - +}); export default Suggestion; diff --git a/packages/editor/core/src/ui/menus/table-menu/InsertBottomTableIcon.tsx b/packages/editor/core/src/ui/menus/table-menu/InsertBottomTableIcon.tsx deleted file mode 100644 index 0e42ba64824..00000000000 --- a/packages/editor/core/src/ui/menus/table-menu/InsertBottomTableIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -const InsertBottomTableIcon = (props: any) => ( - - - -); - -export default InsertBottomTableIcon; diff --git a/packages/editor/core/src/ui/menus/table-menu/InsertLeftTableIcon.tsx b/packages/editor/core/src/ui/menus/table-menu/InsertLeftTableIcon.tsx deleted file mode 100644 index 1fd75fe8754..00000000000 --- a/packages/editor/core/src/ui/menus/table-menu/InsertLeftTableIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const InsertLeftTableIcon = (props: any) => ( - - - -); -export default InsertLeftTableIcon; diff --git a/packages/editor/core/src/ui/menus/table-menu/InsertRightTableIcon.tsx b/packages/editor/core/src/ui/menus/table-menu/InsertRightTableIcon.tsx deleted file mode 100644 index 1a65709694b..00000000000 --- a/packages/editor/core/src/ui/menus/table-menu/InsertRightTableIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -const InsertRightTableIcon = (props: any) => ( - - - -); - -export default InsertRightTableIcon; diff --git a/packages/editor/core/src/ui/menus/table-menu/InsertTopTableIcon.tsx b/packages/editor/core/src/ui/menus/table-menu/InsertTopTableIcon.tsx deleted file mode 100644 index 8f04f4f6126..00000000000 --- a/packages/editor/core/src/ui/menus/table-menu/InsertTopTableIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const InsertTopTableIcon = (props: any) => ( - - - -); -export default InsertTopTableIcon; diff --git a/packages/editor/core/src/ui/menus/table-menu/tooltip.tsx b/packages/editor/core/src/ui/menus/table-menu/tooltip.tsx deleted file mode 100644 index f29d8a49177..00000000000 --- a/packages/editor/core/src/ui/menus/table-menu/tooltip.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as React from 'react'; - -// next-themes -import { useTheme } from "next-themes"; -// tooltip2 -import { Tooltip2 } from "@blueprintjs/popover2"; - -type Props = { - tooltipHeading?: string; - tooltipContent: string | React.ReactNode; - position?: - | "top" - | "right" - | "bottom" - | "left" - | "auto" - | "auto-end" - | "auto-start" - | "bottom-left" - | "bottom-right" - | "left-bottom" - | "left-top" - | "right-bottom" - | "right-top" - | "top-left" - | "top-right"; - children: JSX.Element; - disabled?: boolean; - className?: string; - openDelay?: number; - closeDelay?: number; -}; - -export const Tooltip: React.FC = ({ - tooltipHeading, - tooltipContent, - position = "top", - children, - disabled = false, - className = "", - openDelay = 200, - closeDelay, -}) => { - const { theme } = useTheme(); - - return ( - - {tooltipHeading && ( -
- {tooltipHeading} -
- )} - {tooltipContent} - - } - position={position} - renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) => - React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props }) - } - /> - ); -}; diff --git a/packages/editor/core/src/ui/plugins/delete-image.tsx b/packages/editor/core/src/ui/plugins/delete-image.tsx index 56284472b21..48ec244fcdc 100644 --- a/packages/editor/core/src/ui/plugins/delete-image.tsx +++ b/packages/editor/core/src/ui/plugins/delete-image.tsx @@ -15,7 +15,11 @@ interface ImageNode extends ProseMirrorNode { const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin => new Plugin({ key: deleteKey, - appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { + appendTransaction: ( + transactions: readonly Transaction[], + oldState: EditorState, + newState: EditorState, + ) => { const newImageSources = new Set(); newState.doc.descendants((node) => { if (node.type.name === IMAGE_NODE_TYPE) { @@ -55,7 +59,10 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin => export default TrackImageDeletionPlugin; -async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise { +async function onNodeDeleted( + src: string, + deleteImage: DeleteImage, +): Promise { try { const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); const resStatus = await deleteImage(assetUrlWithWorkspaceId); diff --git a/packages/editor/core/src/ui/plugins/upload-image.tsx b/packages/editor/core/src/ui/plugins/upload-image.tsx index cdd62ae4836..25646007313 100644 --- a/packages/editor/core/src/ui/plugins/upload-image.tsx +++ b/packages/editor/core/src/ui/plugins/upload-image.tsx @@ -4,7 +4,7 @@ import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; const uploadKey = new PluginKey("upload-image"); -const UploadImagesPlugin = () => +const UploadImagesPlugin = (cancelUploadImage?: () => any) => new Plugin({ key: uploadKey, state: { @@ -21,15 +21,46 @@ const UploadImagesPlugin = () => const placeholder = document.createElement("div"); placeholder.setAttribute("class", "img-placeholder"); const image = document.createElement("img"); - image.setAttribute("class", "opacity-10 rounded-lg border border-custom-border-300"); + image.setAttribute( + "class", + "opacity-10 rounded-lg border border-custom-border-300", + ); image.src = src; placeholder.appendChild(image); + + // Create cancel button + const cancelButton = document.createElement("button"); + cancelButton.style.position = "absolute"; + cancelButton.style.right = "3px"; + cancelButton.style.top = "3px"; + cancelButton.setAttribute("class", "opacity-90 rounded-lg"); + + cancelButton.onclick = () => { + cancelUploadImage?.(); + }; + + // Create an SVG element from the SVG string + const svgString = ``; + const parser = new DOMParser(); + const svgElement = parser.parseFromString( + svgString, + "image/svg+xml", + ).documentElement; + + cancelButton.appendChild(svgElement); + placeholder.appendChild(cancelButton); const deco = Decoration.widget(pos + 1, placeholder, { id, }); set = set.add(tr.doc, [deco]); } else if (action && action.remove) { - set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id)); + set = set.remove( + set.find( + undefined, + undefined, + (spec) => spec.id == action.remove.id, + ), + ); } return set; }, @@ -48,19 +79,39 @@ function findPlaceholder(state: EditorState, id: {}) { const found = decos.find( undefined, undefined, - (spec: { id: number | undefined }) => spec.id == id + (spec: { id: number | undefined }) => spec.id == id, ); return found.length ? found[0].from : null; } +const removePlaceholder = (view: EditorView, id: {}) => { + const removePlaceholderTr = view.state.tr.setMeta(uploadKey, { + remove: { id }, + }); + view.dispatch(removePlaceholderTr); +}; + export async function startImageUpload( file: File, view: EditorView, pos: number, uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, ) { + if (!file) { + alert("No file selected. Please select a file to upload."); + return; + } + if (!file.type.includes("image/")) { + alert("Invalid file type. Please select an image file."); + return; + } + + if (file.size > 5 * 1024 * 1024) { + alert("File size too large. Please select a file smaller than 5MB."); return; } @@ -82,28 +133,42 @@ export async function startImageUpload( view.dispatch(tr); }; + // Handle FileReader errors + reader.onerror = (error) => { + console.error("FileReader error: ", error); + removePlaceholder(view, id); + return; + }; + setIsSubmitting?.("submitting"); - const src = await UploadImageHandler(file, uploadFile); - const { schema } = view.state; - pos = findPlaceholder(view.state, id); - - if (pos == null) return; - const imageSrc = typeof src === "object" ? reader.result : src; - - const node = schema.nodes.image.create({ src: imageSrc }); - const transaction = view.state.tr - .replaceWith(pos, pos, node) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); + + try { + const src = await UploadImageHandler(file, uploadFile); + const { schema } = view.state; + pos = findPlaceholder(view.state, id); + + if (pos == null) return; + const imageSrc = typeof src === "object" ? reader.result : src; + + const node = schema.nodes.image.create({ src: imageSrc }); + const transaction = view.state.tr + .replaceWith(pos, pos, node) + .setMeta(uploadKey, { remove: { id } }); + view.dispatch(transaction); + } catch (error) { + console.error("Upload error: ", error); + removePlaceholder(view, id); + } } -const UploadImageHandler = (file: File, - uploadFile: UploadImage +const UploadImageHandler = ( + file: File, + uploadFile: UploadImage, ): Promise => { try { return new Promise(async (resolve, reject) => { try { - const imageUrl = await uploadFile(file) + const imageUrl = await uploadFile(file); const image = new Image(); image.src = imageUrl; @@ -118,9 +183,6 @@ const UploadImageHandler = (file: File, } }); } catch (error) { - if (error instanceof Error) { - console.log(error.message); - } return Promise.reject(error); } }; diff --git a/packages/editor/core/src/ui/props.tsx b/packages/editor/core/src/ui/props.tsx index 8f002b76c4c..865e0d2c7a4 100644 --- a/packages/editor/core/src/ui/props.tsx +++ b/packages/editor/core/src/ui/props.tsx @@ -5,7 +5,9 @@ import { UploadImage } from "../types/upload-image"; export function CoreEditorProps( uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, ): EditorProps { return { attributes: { @@ -32,7 +34,11 @@ export function CoreEditorProps( } } } - if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) { + if ( + event.clipboardData && + event.clipboardData.files && + event.clipboardData.files[0] + ) { event.preventDefault(); const file = event.clipboardData.files[0]; const pos = view.state.selection.from; @@ -51,7 +57,12 @@ export function CoreEditorProps( } } } - if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { + if ( + !moved && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files[0] + ) { event.preventDefault(); const file = event.dataTransfer.files[0]; const coordinates = view.posAtCoords({ @@ -59,7 +70,13 @@ export function CoreEditorProps( top: event.clientY, }); if (coordinates) { - startImageUpload(file, view, coordinates.pos - 1, uploadFile, setIsSubmitting); + startImageUpload( + file, + view, + coordinates.pos - 1, + uploadFile, + setIsSubmitting, + ); } return true; } diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx index 8901d34c517..b8fc9bb952e 100644 --- a/packages/editor/core/src/ui/read-only/extensions.tsx +++ b/packages/editor/core/src/ui/read-only/extensions.tsx @@ -18,9 +18,10 @@ import { isValidHttpUrl } from "../../lib/utils"; import { Mentions } from "../mentions"; import { IMentionSuggestion } from "../../types/mention-suggestion"; -export const CoreReadOnlyEditorExtensions = ( - mentionConfig: { mentionSuggestions: IMentionSuggestion[], mentionHighlights: string[] }, -) => [ +export const CoreReadOnlyEditorExtensions = (mentionConfig: { + mentionSuggestions: IMentionSuggestion[]; + mentionHighlights: string[]; +}) => [ StarterKit.configure({ bulletList: { HTMLAttributes: { @@ -57,41 +58,45 @@ export const CoreReadOnlyEditorExtensions = ( }, gapcursor: false, }), - Gapcursor, - TiptapLink.configure({ - protocols: ["http", "https"], - validate: (url) => isValidHttpUrl(url), - HTMLAttributes: { - class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", - }, - }), - ReadOnlyImageExtension.configure({ - HTMLAttributes: { - class: "rounded-lg border border-custom-border-300", - }, - }), - TiptapUnderline, - TextStyle, - Color, - TaskList.configure({ - HTMLAttributes: { - class: "not-prose pl-2", - }, - }), - TaskItem.configure({ - HTMLAttributes: { - class: "flex items-start my-4", - }, - nested: true, - }), - Markdown.configure({ - html: true, - transformCopiedText: true, - }), - Table, - TableHeader, - TableCell, - TableRow, - Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, true), - ]; + Gapcursor, + TiptapLink.configure({ + protocols: ["http", "https"], + validate: (url) => isValidHttpUrl(url), + HTMLAttributes: { + class: + "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", + }, + }), + ReadOnlyImageExtension.configure({ + HTMLAttributes: { + class: "rounded-lg border border-custom-border-300", + }, + }), + TiptapUnderline, + TextStyle, + Color, + TaskList.configure({ + HTMLAttributes: { + class: "not-prose pl-2", + }, + }), + TaskItem.configure({ + HTMLAttributes: { + class: "flex items-start my-4", + }, + nested: true, + }), + Markdown.configure({ + html: true, + transformCopiedText: true, + }), + Table, + TableHeader, + TableCell, + TableRow, + Mentions( + mentionConfig.mentionSuggestions, + mentionConfig.mentionHighlights, + true, + ), +]; diff --git a/packages/editor/core/src/ui/read-only/props.tsx b/packages/editor/core/src/ui/read-only/props.tsx index 25db2b68c1a..79f9fcb0dbb 100644 --- a/packages/editor/core/src/ui/read-only/props.tsx +++ b/packages/editor/core/src/ui/read-only/props.tsx @@ -1,7 +1,6 @@ import { EditorProps } from "@tiptap/pm/view"; -export const CoreReadOnlyEditorProps: EditorProps = -{ +export const CoreReadOnlyEditorProps: EditorProps = { attributes: { class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, }, diff --git a/packages/editor/lite-text-editor/Readme.md b/packages/editor/lite-text-editor/Readme.md index 948e2c34b2d..1f10f5ff42e 100644 --- a/packages/editor/lite-text-editor/Readme.md +++ b/packages/editor/lite-text-editor/Readme.md @@ -10,25 +10,25 @@ The `@plane/lite-text-editor` package extends from the `editor-core` package, in `LiteTextEditor` & `LiteTextEditorWithRef` -- **Read Only Editor Instances**: We have added a really light weight *Read Only* Editor instance for the Lite editor types (with and without Ref) +- **Read Only Editor Instances**: We have added a really light weight _Read Only_ Editor instance for the Lite editor types (with and without Ref) `LiteReadOnlyEditor` &`LiteReadOnlyEditorWithRef` ## LiteTextEditor -| Prop | Type | Description | -| --- | --- | --- | -| `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | -| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | -| `value` | `html string` | The initial content of the editor. | -| `onEnterKeyPress` | `(e) => void` | The event that happens on Enter key press | -| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | -| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | -| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | -| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". | -| `noBorder` | `boolean` | If set to true, the editor will not have a border. | -| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | -| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | -| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | +| Prop | Type | Description | +| ------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | +| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | +| `value` | `html string` | The initial content of the editor. | +| `onEnterKeyPress` | `(e) => void` | The event that happens on Enter key press | +| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | +| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | +| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | +| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". | +| `noBorder` | `boolean` | If set to true, the editor will not have a border. | +| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | +| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | +| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | ### Usage @@ -36,62 +36,62 @@ The `@plane/lite-text-editor` package extends from the `editor-core` package, in ```tsx { - onChange(comment_html); - }} - /> + onEnterKeyPress={handleSubmit(handleCommentUpdate)} + uploadFile={fileService.getUploadFileFunction(workspaceSlug)} + deleteFile={fileService.deleteImage} + value={value} + debouncedUpdatesEnabled={false} + customClassName="min-h-[50px] p-3 shadow-sm" + onChange={(comment_json: Object, comment_html: string) => { + onChange(comment_html); + }} +/> ``` 2. Example of how to use the `LiteTextEditorWithRef` component ```tsx - const editorRef = useRef(null); - - // can use it to set the editor's value - editorRef.current?.setEditorValue(`${watch("description_html")}`); - - // can use it to clear the editor - editorRef?.current?.clearEditor(); - - return ( - { - onChange(comment_html); - }} - /> -) +const editorRef = useRef(null); + +// can use it to set the editor's value +editorRef.current?.setEditorValue(`${watch("description_html")}`); + +// can use it to clear the editor +editorRef?.current?.clearEditor(); + +return ( + { + onChange(comment_html); + }} + /> +); ``` ## LiteReadOnlyEditor -| Prop | Type | Description | -| --- | --- | --- | -| `value` | `html string` | The initial content of the editor. | -| `noBorder` | `boolean` | If set to true, the editor will not have a border. | -| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | -| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | -| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | +| Prop | Type | Description | +| ------------------------------- | ------------- | --------------------------------------------------------------------- | +| `value` | `html string` | The initial content of the editor. | +| `noBorder` | `boolean` | If set to true, the editor will not have a border. | +| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | +| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | +| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | ### Usage Here is an example of how to use the `RichReadOnlyEditor` component ```tsx - + ``` diff --git a/packages/editor/lite-text-editor/package.json b/packages/editor/lite-text-editor/package.json index 47ef154c69e..52f27fb291c 100644 --- a/packages/editor/lite-text-editor/package.json +++ b/packages/editor/lite-text-editor/package.json @@ -2,6 +2,7 @@ "name": "@plane/lite-text-editor", "version": "0.0.1", "description": "Package that powers Plane's Comment Editor", + "private": true, "main": "./dist/index.mjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", @@ -28,10 +29,8 @@ }, "dependencies": { "@plane/editor-core": "*", + "@plane/ui": "*", "@tiptap/extension-list-item": "^2.1.11", - "@types/node": "18.15.3", - "@types/react": "^18.2.5", - "@types/react-dom": "18.0.11", "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", "eslint": "8.36.0", @@ -46,6 +45,9 @@ "use-debounce": "^9.0.4" }, "devDependencies": { + "@types/node": "18.15.3", + "@types/react": "^18.2.35", + "@types/react-dom": "^18.2.14", "eslint": "^7.32.0", "postcss": "^8.4.29", "tailwind-config-custom": "*", diff --git a/packages/editor/lite-text-editor/src/index.ts b/packages/editor/lite-text-editor/src/index.ts index 392928ccffd..ba916e666d6 100644 --- a/packages/editor/lite-text-editor/src/index.ts +++ b/packages/editor/lite-text-editor/src/index.ts @@ -1,3 +1,3 @@ export { LiteTextEditor, LiteTextEditorWithRef } from "./ui"; export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only"; -export type { IMentionSuggestion, IMentionHighlight } from "./ui" +export type { IMentionSuggestion, IMentionHighlight } from "./ui"; diff --git a/packages/editor/lite-text-editor/src/ui/index.tsx b/packages/editor/lite-text-editor/src/ui/index.tsx index 6cd03bcfa3e..e7decbcac80 100644 --- a/packages/editor/lite-text-editor/src/ui/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/index.tsx @@ -1,4 +1,3 @@ -"use client"; import * as React from "react"; import { EditorContainer, @@ -48,6 +47,7 @@ interface ILiteTextEditor { }[]; }; onEnterKeyPress?: (e?: any) => void; + cancelUploadImage?: () => any; mentionHighlights?: string[]; mentionSuggestions?: IMentionSuggestion[]; submitButton?: React.ReactNode; @@ -65,6 +65,7 @@ interface EditorHandle { const LiteTextEditor = (props: LiteTextEditorProps) => { const { onChange, + cancelUploadImage, debouncedUpdatesEnabled, setIsSubmitting, setShouldShowAlert, @@ -85,6 +86,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => { const editor = useEditor({ onChange, + cancelUploadImage, debouncedUpdatesEnabled, setIsSubmitting, setShouldShowAlert, diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx index c0006b3f257..60878f9bf2d 100644 --- a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/icon.tsx @@ -6,8 +6,9 @@ type Props = { }; export const Icon: React.FC = ({ iconName, className = "" }) => ( - + {iconName} ); - diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx index cf0d78688fa..a4fb0479c17 100644 --- a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx @@ -14,8 +14,8 @@ import { TableItem, UnderLineItem, } from "@plane/editor-core"; -import { Tooltip } from "../../tooltip"; -import { UploadImage } from "../.."; +import { Tooltip } from "@plane/ui"; +import { UploadImage } from "../../"; export interface BubbleMenuItem { name: string; diff --git a/packages/editor/lite-text-editor/src/ui/read-only/index.tsx b/packages/editor/lite-text-editor/src/ui/read-only/index.tsx index 5dccbe97731..a3de061ae48 100644 --- a/packages/editor/lite-text-editor/src/ui/read-only/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/read-only/index.tsx @@ -1,6 +1,10 @@ -"use client" -import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from '@plane/editor-core'; -import * as React from 'react'; +import * as React from "react"; +import { + EditorContainer, + EditorContentWrapper, + getEditorClassNames, + useReadOnlyEditor, +} from "@plane/editor-core"; interface ICoreReadOnlyEditor { value: string; @@ -8,7 +12,7 @@ interface ICoreReadOnlyEditor { noBorder?: boolean; borderOnFocus?: boolean; customClassName?: string; - mentionHighlights: string[] + mentionHighlights: string[]; } interface EditorCoreProps extends ICoreReadOnlyEditor { @@ -27,31 +31,39 @@ const LiteReadOnlyEditor = ({ customClassName, value, forwardedRef, - mentionHighlights + mentionHighlights, }: EditorCoreProps) => { const editor = useReadOnlyEditor({ value, forwardedRef, - mentionHighlights + mentionHighlights, }); - const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); + const editorClassNames = getEditorClassNames({ + noBorder, + borderOnFocus, + customClassName, + }); if (!editor) return null; return (
- +
-
+ ); }; -const LiteReadOnlyEditorWithRef = React.forwardRef((props, ref) => ( - -)); +const LiteReadOnlyEditorWithRef = React.forwardRef< + EditorHandle, + ICoreReadOnlyEditor +>((props, ref) => ); LiteReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef"; -export { LiteReadOnlyEditor , LiteReadOnlyEditorWithRef }; +export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef }; diff --git a/packages/editor/lite-text-editor/src/ui/tooltip.tsx b/packages/editor/lite-text-editor/src/ui/tooltip.tsx index f29d8a49177..a2f2414e529 100644 --- a/packages/editor/lite-text-editor/src/ui/tooltip.tsx +++ b/packages/editor/lite-text-editor/src/ui/tooltip.tsx @@ -1,5 +1,4 @@ -import * as React from 'react'; - +import * as React from "react"; // next-themes import { useTheme } from "next-themes"; // tooltip2 @@ -69,8 +68,16 @@ export const Tooltip: React.FC = ({ } position={position} - renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) => - React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props }) + renderTarget={({ + isOpen: isTooltipOpen, + ref: eleReference, + ...tooltipProps + }) => + React.cloneElement(children, { + ref: eleReference, + ...tooltipProps, + ...children.props, + }) } /> ); diff --git a/packages/editor/rich-text-editor/Readme.md b/packages/editor/rich-text-editor/Readme.md index c8414f62d6a..44ed9ba5e4b 100644 --- a/packages/editor/rich-text-editor/Readme.md +++ b/packages/editor/rich-text-editor/Readme.md @@ -10,24 +10,24 @@ The `@plane/rich-text-editor` package extends from the `editor-core` package, in `RichTextEditor` & `RichTextEditorWithRef` -- **Read Only Editor Instances**: We have added a really light weight *Read Only* Editor instance for the Rich editor types (with and without Ref) +- **Read Only Editor Instances**: We have added a really light weight _Read Only_ Editor instance for the Rich editor types (with and without Ref) `RichReadOnlyEditor` &`RichReadOnlyEditorWithRef` ## RichTextEditor -| Prop | Type | Description | -| --- | --- | --- | -| `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | -| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | -| `value` | `html string` | The initial content of the editor. | -| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | -| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | -| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | -| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". | -| `noBorder` | `boolean` | If set to true, the editor will not have a border. | -| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | -| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | -| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | +| Prop | Type | Description | +| ------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `uploadFile` | `(file: File) => Promise` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. | +| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. | +| `value` | `html string` | The initial content of the editor. | +| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. | +| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. | +| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. | +| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". | +| `noBorder` | `boolean` | If set to true, the editor will not have a border. | +| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | +| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | +| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | ### Usage @@ -57,43 +57,47 @@ The `@plane/rich-text-editor` package extends from the `editor-core` package, in 2. Example of how to use the `RichTextEditorWithRef` component ```tsx - const editorRef = useRef(null); - - // can use it to set the editor's value - editorRef.current?.setEditorValue(`${watch("description_html")}`); - - // can use it to clear the editor - editorRef?.current?.clearEditor(); - - return ( { - onChange(description_html); - // custom stuff you want to do - } } />) +const editorRef = useRef(null); + +// can use it to set the editor's value +editorRef.current?.setEditorValue(`${watch("description_html")}`); + +// can use it to clear the editor +editorRef?.current?.clearEditor(); + +return ( + { + onChange(description_html); + // custom stuff you want to do + }} + /> +); ``` ## RichReadOnlyEditor -| Prop | Type | Description | -| --- | --- | --- | -| `value` | `html string` | The initial content of the editor. | -| `noBorder` | `boolean` | If set to true, the editor will not have a border. | -| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | -| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | -| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | +| Prop | Type | Description | +| ------------------------------- | ------------- | --------------------------------------------------------------------- | +| `value` | `html string` | The initial content of the editor. | +| `noBorder` | `boolean` | If set to true, the editor will not have a border. | +| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. | +| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. | +| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. | ### Usage Here is an example of how to use the `RichReadOnlyEditor` component ```tsx - + ``` diff --git a/packages/editor/rich-text-editor/package.json b/packages/editor/rich-text-editor/package.json index 7bdd0a58b1d..db793261cc2 100644 --- a/packages/editor/rich-text-editor/package.json +++ b/packages/editor/rich-text-editor/package.json @@ -2,6 +2,7 @@ "name": "@plane/rich-text-editor", "version": "0.0.1", "description": "Rich Text Editor that powers Plane", + "private": true, "main": "./dist/index.mjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", @@ -21,19 +22,19 @@ "check-types": "tsc --noEmit" }, "peerDependencies": { + "@tiptap/core": "^2.1.11", "next": "12.3.2", "next-themes": "^0.2.1", "react": "^18.2.0", - "react-dom": "18.2.0", - "@tiptap/core": "^2.1.11" + "react-dom": "18.2.0" }, "dependencies": { "@plane/editor-core": "*", "@tiptap/extension-code-block-lowlight": "^2.1.11", "@tiptap/extension-horizontal-rule": "^2.1.11", "@tiptap/extension-placeholder": "^2.1.11", - "class-variance-authority": "^0.7.0", "@tiptap/suggestion": "^2.1.7", + "class-variance-authority": "^0.7.0", "clsx": "^1.2.1", "highlight.js": "^11.8.0", "lowlight": "^3.0.0", @@ -41,8 +42,8 @@ }, "devDependencies": { "@types/node": "18.15.3", - "@types/react": "^18.2.5", - "@types/react-dom": "18.0.11", + "@types/react": "^18.2.35", + "@types/react-dom": "^18.2.14", "eslint": "^7.32.0", "postcss": "^8.4.29", "react": "^18.2.0", diff --git a/packages/editor/rich-text-editor/src/index.ts b/packages/editor/rich-text-editor/src/index.ts index e296a617171..9ea7f9a39e3 100644 --- a/packages/editor/rich-text-editor/src/index.ts +++ b/packages/editor/rich-text-editor/src/index.ts @@ -2,4 +2,4 @@ import "./styles/github-dark.css"; export { RichTextEditor, RichTextEditorWithRef } from "./ui"; export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only"; -export type { IMentionSuggestion, IMentionHighlight } from "./ui" +export type { IMentionSuggestion, IMentionHighlight } from "./ui"; diff --git a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx index f0f3bed3423..a28982da31e 100644 --- a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx @@ -1,7 +1,7 @@ import HorizontalRule from "@tiptap/extension-horizontal-rule"; import Placeholder from "@tiptap/extension-placeholder"; import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; -import { common, createLowlight } from 'lowlight' +import { common, createLowlight } from "lowlight"; import { InputRule } from "@tiptap/core"; import ts from "highlight.js/lib/languages/typescript"; @@ -9,51 +9,53 @@ import ts from "highlight.js/lib/languages/typescript"; import SlashCommand from "./slash-command"; import { UploadImage } from "../"; -const lowlight = createLowlight(common) +const lowlight = createLowlight(common); lowlight.register("ts", ts); export const RichTextEditorExtensions = ( uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, ) => [ - HorizontalRule.extend({ - addInputRules() { - return [ - new InputRule({ - find: /^(?:---|—-|___\s|\*\*\*\s)$/, - handler: ({ state, range, commands }) => { - commands.splitBlock(); + HorizontalRule.extend({ + addInputRules() { + return [ + new InputRule({ + find: /^(?:---|—-|___\s|\*\*\*\s)$/, + handler: ({ state, range, commands }) => { + commands.splitBlock(); - const attributes = {}; - const { tr } = state; - const start = range.from; - const end = range.to; - // @ts-ignore - tr.replaceWith(start - 1, end, this.type.create(attributes)); - }, - }), - ]; - }, - }).configure({ - HTMLAttributes: { - class: "mb-6 border-t border-custom-border-300", - }, - }), - SlashCommand(uploadFile, setIsSubmitting), - CodeBlockLowlight.configure({ - lowlight, - }), - Placeholder.configure({ - placeholder: ({ node }) => { - if (node.type.name === "heading") { - return `Heading ${node.attrs.level}`; - } - if (node.type.name === "image" || node.type.name === "table") { - return ""; - } + const attributes = {}; + const { tr } = state; + const start = range.from; + const end = range.to; + // @ts-ignore + tr.replaceWith(start - 1, end, this.type.create(attributes)); + }, + }), + ]; + }, + }).configure({ + HTMLAttributes: { + class: "mb-6 border-t border-custom-border-300", + }, + }), + SlashCommand(uploadFile, setIsSubmitting), + CodeBlockLowlight.configure({ + lowlight, + }), + Placeholder.configure({ + placeholder: ({ node }) => { + if (node.type.name === "heading") { + return `Heading ${node.attrs.level}`; + } + if (node.type.name === "image" || node.type.name === "table") { + return ""; + } - return "Press '/' for commands..."; - }, - includeChildren: true, - }), - ]; + return "Press '/' for commands..."; + }, + includeChildren: true, + }), +]; diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index a0dbe7226e6..2e98a72aae3 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -1,8 +1,13 @@ -"use client" -import * as React from 'react'; -import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from '@plane/editor-core'; -import { EditorBubbleMenu } from './menus/bubble-menu'; -import { RichTextEditorExtensions } from './extensions'; +"use client"; +import * as React from "react"; +import { + EditorContainer, + EditorContentWrapper, + getEditorClassNames, + useEditor, +} from "@plane/editor-core"; +import { EditorBubbleMenu } from "./menus/bubble-menu"; +import { RichTextEditorExtensions } from "./extensions"; export type UploadImage = (file: File) => Promise; export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; @@ -14,9 +19,9 @@ export type IMentionSuggestion = { title: string; subtitle: string; redirect_uri: string; -} +}; -export type IMentionHighlight = string +export type IMentionHighlight = string; interface IRichTextEditor { value: string; @@ -24,10 +29,13 @@ interface IRichTextEditor { deleteFile: DeleteImage; noBorder?: boolean; borderOnFocus?: boolean; + cancelUploadImage?: () => any; customClassName?: string; editorContentCustomClassNames?: string; onChange?: (json: any, html: string) => void; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void; setShouldShowAlert?: (showAlert: boolean) => void; forwardedRef?: any; debouncedUpdatesEnabled?: boolean; @@ -54,11 +62,12 @@ const RichTextEditor = ({ uploadFile, deleteFile, noBorder, + cancelUploadImage, borderOnFocus, customClassName, forwardedRef, mentionHighlights, - mentionSuggestions + mentionSuggestions, }: RichTextEditorProps) => { const editor = useEditor({ onChange, @@ -67,14 +76,19 @@ const RichTextEditor = ({ setShouldShowAlert, value, uploadFile, + cancelUploadImage, deleteFile, forwardedRef, extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting), mentionHighlights, - mentionSuggestions + mentionSuggestions, }); - const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); + const editorClassNames = getEditorClassNames({ + noBorder, + borderOnFocus, + customClassName, + }); if (!editor) return null; @@ -82,16 +96,19 @@ const RichTextEditor = ({ {editor && }
- +
-
+ ); }; -const RichTextEditorWithRef = React.forwardRef((props, ref) => ( - -)); +const RichTextEditorWithRef = React.forwardRef( + (props, ref) => , +); RichTextEditorWithRef.displayName = "RichTextEditorWithRef"; -export { RichTextEditor, RichTextEditorWithRef}; +export { RichTextEditor, RichTextEditorWithRef }; diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx index 7dddc9d984b..f8f1f17bbe6 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/link-selector.tsx @@ -1,7 +1,19 @@ import { Editor } from "@tiptap/core"; import { Check, Trash } from "lucide-react"; -import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react"; -import { cn, isValidHttpUrl, setLinkEditor, unsetLinkEditor, } from "@plane/editor-core"; +import { + Dispatch, + FC, + SetStateAction, + useCallback, + useEffect, + useRef, +} from "react"; +import { + cn, + isValidHttpUrl, + setLinkEditor, + unsetLinkEditor, +} from "@plane/editor-core"; interface LinkSelectorProps { editor: Editor; @@ -9,7 +21,11 @@ interface LinkSelectorProps { setIsOpen: Dispatch>; } -export const LinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { +export const LinkSelector: FC = ({ + editor, + isOpen, + setIsOpen, +}) => { const inputRef = useRef(null); const onLinkSubmit = useCallback(() => { @@ -31,7 +47,7 @@ export const LinkSelector: FC = ({ editor, isOpen, setIsOpen type="button" className={cn( "flex h-full items-center space-x-2 px-3 py-1.5 text-sm font-medium text-custom-text-300 hover:bg-custom-background-100 active:bg-custom-background-100", - { "bg-custom-background-100": isOpen } + { "bg-custom-background-100": isOpen }, )} onClick={() => { setIsOpen(!isOpen); diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx index b8b7ffc58db..965e7a42e4c 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx @@ -1,10 +1,16 @@ -import { BulletListItem, cn, CodeItem, HeadingOneItem, HeadingThreeItem, HeadingTwoItem, NumberedListItem, QuoteItem, TodoListItem } from "@plane/editor-core"; -import { Editor } from "@tiptap/react"; import { - Check, - ChevronDown, - TextIcon, -} from "lucide-react"; + BulletListItem, + cn, + CodeItem, + HeadingOneItem, + HeadingThreeItem, + HeadingTwoItem, + NumberedListItem, + QuoteItem, + TodoListItem, +} from "@plane/editor-core"; +import { Editor } from "@tiptap/react"; +import { Check, ChevronDown, TextIcon } from "lucide-react"; import { Dispatch, FC, SetStateAction } from "react"; import { BubbleMenuItem } from "."; @@ -15,12 +21,17 @@ interface NodeSelectorProps { setIsOpen: Dispatch>; } -export const NodeSelector: FC = ({ editor, isOpen, setIsOpen }) => { +export const NodeSelector: FC = ({ + editor, + isOpen, + setIsOpen, +}) => { const items: BubbleMenuItem[] = [ { name: "Text", icon: TextIcon, - command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(), + command: () => + editor.chain().focus().toggleNode("paragraph", "paragraph").run(), isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && @@ -63,7 +74,10 @@ export const NodeSelector: FC = ({ editor, isOpen, setIsOpen }} className={cn( "flex items-center justify-between rounded-sm px-2 py-1 text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100", - { "bg-custom-primary-100/5 text-custom-text-100": activeItem.name === item.name } + { + "bg-custom-primary-100/5 text-custom-text-100": + activeItem.name === item.name, + }, )} >
diff --git a/packages/editor/rich-text-editor/src/ui/read-only/index.tsx b/packages/editor/rich-text-editor/src/ui/read-only/index.tsx index dc058cf8937..f6ccdddf590 100644 --- a/packages/editor/rich-text-editor/src/ui/read-only/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/read-only/index.tsx @@ -1,6 +1,11 @@ -"use client" -import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from '@plane/editor-core'; -import * as React from 'react'; +"use client"; +import { + EditorContainer, + EditorContentWrapper, + getEditorClassNames, + useReadOnlyEditor, +} from "@plane/editor-core"; +import * as React from "react"; interface IRichTextReadOnlyEditor { value: string; @@ -8,6 +13,7 @@ interface IRichTextReadOnlyEditor { noBorder?: boolean; borderOnFocus?: boolean; customClassName?: string; + mentionHighlights?: string[]; } interface RichTextReadOnlyEditorProps extends IRichTextReadOnlyEditor { @@ -26,29 +32,39 @@ const RichReadOnlyEditor = ({ customClassName, value, forwardedRef, + mentionHighlights, }: RichTextReadOnlyEditorProps) => { const editor = useReadOnlyEditor({ value, forwardedRef, + mentionHighlights, }); - const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); + const editorClassNames = getEditorClassNames({ + noBorder, + borderOnFocus, + customClassName, + }); if (!editor) return null; return (
- +
-
+ ); }; -const RichReadOnlyEditorWithRef = React.forwardRef((props, ref) => ( - -)); +const RichReadOnlyEditorWithRef = React.forwardRef< + EditorHandle, + IRichTextReadOnlyEditor +>((props, ref) => ); RichReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef"; -export { RichReadOnlyEditor , RichReadOnlyEditorWithRef }; +export { RichReadOnlyEditor, RichReadOnlyEditorWithRef }; diff --git a/packages/eslint-config-custom/package.json b/packages/eslint-config-custom/package.json index 12a7ab8c8da..11e970d0e23 100644 --- a/packages/eslint-config-custom/package.json +++ b/packages/eslint-config-custom/package.json @@ -1,5 +1,6 @@ { "name": "eslint-config-custom", + "private": true, "version": "0.13.2", "main": "index.js", "license": "MIT", diff --git a/packages/tailwind-config-custom/package.json b/packages/tailwind-config-custom/package.json index 1336379b713..286dfc3b686 100644 --- a/packages/tailwind-config-custom/package.json +++ b/packages/tailwind-config-custom/package.json @@ -3,6 +3,7 @@ "version": "0.13.2", "description": "common tailwind configuration across monorepo", "main": "index.js", + "private": true, "devDependencies": { "@tailwindcss/typography": "^0.5.9", "autoprefixer": "^10.4.14", diff --git a/packages/ui/package.json b/packages/ui/package.json index f76bd837460..72413eb7cb0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,5 +1,7 @@ { "name": "@plane/ui", + "description": "UI components shared across multiple apps internally", + "private": true, "version": "0.0.1", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/ui/src/avatar/avatar-group.tsx b/packages/ui/src/avatar/avatar-group.tsx index 4abb4a93b90..25a3c76fc5f 100644 --- a/packages/ui/src/avatar/avatar-group.tsx +++ b/packages/ui/src/avatar/avatar-group.tsx @@ -35,8 +35,11 @@ export const AvatarGroup: React.FC = (props) => { // calculate total length of avatars inside the group const totalAvatars = React.Children.toArray(children).length; + // if avatars are equal to max + 1, then we need to show the last avatar as well, if avatars are more than max + 1, then we need to show the count of the remaining avatars + const maxAvatarsToRender = totalAvatars <= max + 1 ? max + 1 : max; + // slice the children to the maximum number of avatars - const avatars = React.Children.toArray(children).slice(0, max); + const avatars = React.Children.toArray(children).slice(0, maxAvatarsToRender); // assign the necessary props from the AvatarGroup component to the Avatar components const avatarsWithUpdatedProps = avatars.map((avatar) => { @@ -54,11 +57,14 @@ export const AvatarGroup: React.FC = (props) => { return (
{avatarsWithUpdatedProps.map((avatar, index) => ( -
+
{avatar}
))} - {max < totalAvatars && ( + {maxAvatarsToRender < totalAvatars && ( = (props) => {
{ case "circle": return "rounded-full"; case "square": - return "rounded-md"; + return "rounded"; default: return "rounded-full"; } @@ -119,6 +123,7 @@ export const Avatar: React.FC = (props) => { size = "md", shape = "circle", src, + className = "", } = props; // get size details based on the size prop @@ -145,14 +150,16 @@ export const Avatar: React.FC = (props) => { {src ? ( {name} ) : (
( )} ); - } + }, ); Button.displayName = "plane-ui-button"; diff --git a/packages/ui/src/button/helper.tsx b/packages/ui/src/button/helper.tsx index 82489c3e81a..48b1fc94a0f 100644 --- a/packages/ui/src/button/helper.tsx +++ b/packages/ui/src/button/helper.tsx @@ -102,7 +102,7 @@ export const buttonStyling: IButtonStyling = { export const getButtonStyling = ( variant: TButtonVariant, size: TButtonSizes, - disabled: boolean = false + disabled: boolean = false, ): string => { let _variant: string = ``; const currentVariant = buttonStyling[variant]; diff --git a/packages/ui/src/dropdowns/custom-search-select.tsx b/packages/ui/src/dropdowns/custom-search-select.tsx index 8ba95c28cc6..0fb4c67cf32 100644 --- a/packages/ui/src/dropdowns/custom-search-select.tsx +++ b/packages/ui/src/dropdowns/custom-search-select.tsx @@ -35,7 +35,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState( - null + null, ); const { styles, attributes } = usePopper(referenceElement, popperElement, { @@ -46,7 +46,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { query === "" ? options : options?.filter((option) => - option.query.toLowerCase().includes(query.toLowerCase()) + option.query.toLowerCase().includes(query.toLowerCase()), ); const comboboxProps: any = { @@ -87,8 +87,8 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { + } /> )} /> - - { - userStore.requiredLogin(() => { - handleSubmit(onSubmit)(e); - }); - }} - type="submit" - disabled={isSubmitting || disabled} - className="mt-2" - > - {isSubmitting ? "Adding..." : "Comment"} -
); 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 b4754f09836..ab09b2490ee 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -15,6 +15,7 @@ import { timeAgo } from "helpers/date-time.helper"; import { Comment } from "types/issue"; // services import fileService from "services/file.service"; +import useEditorSuggestions from "hooks/use-editor-suggestions"; type Props = { workspaceSlug: string; @@ -28,6 +29,8 @@ export const CommentCard: React.FC = observer((props) => { // states const [isEditing, setIsEditing] = useState(false); + const mentionsConfig = useEditorSuggestions(); + const editorRef = React.useRef(null); const showEditorRef = React.useRef(null); @@ -100,6 +103,7 @@ export const CommentCard: React.FC = observer((props) => { render={({ field: { onChange, value } }) => ( = observer((props) => { ref={showEditorRef} value={comment.comment_html} customClassName="text-xs border border-custom-border-200 bg-custom-background-100" + mentionHighlights={mentionsConfig.mentionHighlights} />
diff --git a/space/components/issues/peek-overview/issue-details.tsx b/space/components/issues/peek-overview/issue-details.tsx index 24dd656513b..d84103388dc 100644 --- a/space/components/issues/peek-overview/issue-details.tsx +++ b/space/components/issues/peek-overview/issue-details.tsx @@ -2,27 +2,36 @@ import { IssueReactions } from "components/issues/peek-overview"; import { RichReadOnlyEditor } from "@plane/rich-text-editor"; // types import { IIssue } from "types/issue"; +import useEditorSuggestions from "hooks/use-editor-suggestions"; type Props = { issueDetails: IIssue; }; -export const PeekOverviewIssueDetails: React.FC = ({ issueDetails }) => ( -
-
- {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} -
-

{issueDetails.name}

- {issueDetails.description_html !== "" && issueDetails.description_html !== "

" && ( -

" - : issueDetails.description_html} - customClassName="p-3 min-h-[50px] shadow-sm" /> - )} - -
-); +export const PeekOverviewIssueDetails: React.FC = ({ issueDetails }) => { + const mentionConfig = useEditorSuggestions(); + + return ( +
+
+ {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} +
+

{issueDetails.name}

+ {issueDetails.description_html !== "" && issueDetails.description_html !== "

" && ( +

" + : issueDetails.description_html + } + customClassName="p-3 min-h-[50px] shadow-sm" + mentionHighlights={mentionConfig.mentionHighlights} + /> + )} + +
+ ); +}; diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx index f7ccab18f08..54e9c4f6afb 100644 --- a/space/components/issues/peek-overview/issue-properties.tsx +++ b/space/components/issues/peek-overview/issue-properties.tsx @@ -1,5 +1,7 @@ // hooks import useToast from "hooks/use-toast"; +// ui +import { StateGroupIcon } from "@plane/ui"; // icons import { Icon } from "components/ui"; // helpers @@ -63,7 +65,7 @@ export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mod {stateGroup && (
- + {addSpaceIfCamelCase(state?.name ?? "")}
diff --git a/space/constants/data.ts b/space/constants/data.ts index 29d411342fb..bb903069661 100644 --- a/space/constants/data.ts +++ b/space/constants/data.ts @@ -1,6 +1,5 @@ // interfaces import { - IIssueBoardViews, // priority TIssuePriorityKey, // state groups @@ -8,14 +7,6 @@ import { IIssuePriorityFilters, IIssueGroup, } from "types/issue"; -// icons -import { - BacklogStateIcon, - UnstartedStateIcon, - StartedStateIcon, - CompletedStateIcon, - CancelledStateIcon, -} from "components/icons"; // all issue views export const issueViews: any = { @@ -92,35 +83,30 @@ export const issueGroups: IIssueGroup[] = [ title: "Backlog", color: "#d9d9d9", className: `text-[#d9d9d9] bg-[#d9d9d9]/10`, - icon: BacklogStateIcon, }, { key: "unstarted", title: "Unstarted", color: "#3f76ff", className: `text-[#3f76ff] bg-[#3f76ff]/10`, - icon: UnstartedStateIcon, }, { key: "started", title: "Started", color: "#f59e0b", className: `text-[#f59e0b] bg-[#f59e0b]/10`, - icon: StartedStateIcon, }, { key: "completed", title: "Completed", color: "#16a34a", className: `text-[#16a34a] bg-[#16a34a]/10`, - icon: CompletedStateIcon, }, { key: "cancelled", title: "Cancelled", color: "#dc2626", className: `text-[#dc2626] bg-[#dc2626]/10`, - icon: CancelledStateIcon, }, ]; diff --git a/space/hooks/use-editor-suggestions.tsx b/space/hooks/use-editor-suggestions.tsx new file mode 100644 index 00000000000..0659121b7cd --- /dev/null +++ b/space/hooks/use-editor-suggestions.tsx @@ -0,0 +1,13 @@ +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const useEditorSuggestions = () => { + const { mentionsStore }: RootStore = useMobxStore(); + + return { + // mentionSuggestions: mentionsStore.mentionSuggestions, + mentionHighlights: mentionsStore.mentionHighlights, + }; +}; + +export default useEditorSuggestions; diff --git a/space/services/app-config.service.ts b/space/services/app-config.service.ts index 713cda3da1b..09a6989efe1 100644 --- a/space/services/app-config.service.ts +++ b/space/services/app-config.service.ts @@ -3,12 +3,13 @@ import APIService from "services/api.service"; // helper import { API_BASE_URL } from "helpers/common.helper"; -export interface IEnvConfig { - github: string; - google: string; - github_app_name: string | null; +export interface IAppConfig { email_password_login: boolean; + google_client_id: string | null; + github_app_name: string | null; + github_client_id: string | null; magic_login: boolean; + slack_client_id: string | null; } export class AppConfigService extends APIService { @@ -16,7 +17,7 @@ export class AppConfigService extends APIService { super(API_BASE_URL); } - async envConfig(): Promise { + async envConfig(): Promise { return this.get("/api/configs/", { headers: { "Content-Type": "application/json", diff --git a/space/services/file.service.ts b/space/services/file.service.ts index 1ba4cd4d292..ce1f50e7031 100644 --- a/space/services/file.service.ts +++ b/space/services/file.service.ts @@ -1,5 +1,6 @@ import APIService from "services/api.service"; import { API_BASE_URL } from "helpers/common.helper"; +import axios from "axios"; interface UnSplashImage { id: string; @@ -26,25 +27,37 @@ interface UnSplashImageUrls { } class FileService extends APIService { + private cancelSource: any; + constructor() { super(API_BASE_URL); this.uploadFile = this.uploadFile.bind(this); this.deleteImage = this.deleteImage.bind(this); + this.cancelUpload = this.cancelUpload.bind(this); } async uploadFile(workspaceSlug: string, file: FormData): Promise { + this.cancelSource = axios.CancelToken.source(); return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, { headers: { ...this.getHeaders(), "Content-Type": "multipart/form-data", }, + cancelToken: this.cancelSource.token, }) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + if (axios.isCancel(error)) { + console.log(error.message); + } else { + throw error?.response?.data; + } }); } + cancelUpload() { + this.cancelSource.cancel("Upload cancelled"); + } getUploadFileFunction(workspaceSlug: string): (file: File) => Promise { return async (file: File) => { const formData = new FormData(); diff --git a/space/store/mentions.store.ts b/space/store/mentions.store.ts new file mode 100644 index 00000000000..e890681d3a8 --- /dev/null +++ b/space/store/mentions.store.ts @@ -0,0 +1,43 @@ +import { IMentionHighlight } from "@plane/lite-text-editor"; +import { RootStore } from "./root"; +import { computed, makeObservable } from "mobx"; + +export interface IMentionsStore { + // mentionSuggestions: IMentionSuggestion[]; + mentionHighlights: IMentionHighlight[]; +} + +export class MentionsStore implements IMentionsStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + // rootStore + this.rootStore = _rootStore; + + makeObservable(this, { + mentionHighlights: computed, + // mentionSuggestions: computed + }); + } + + // get mentionSuggestions() { + // const projectMembers = this.rootStore.project.project. + + // const suggestions = projectMembers === null ? [] : projectMembers.map((member) => ({ + // id: member.member.id, + // type: "User", + // title: member.member.display_name, + // subtitle: member.member.email ?? "", + // avatar: member.member.avatar, + // redirect_uri: `/${member.workspace.slug}/profile/${member.member.id}`, + // })) + + // return suggestions + // } + + get mentionHighlights() { + const user = this.rootStore.user.currentUser; + return user ? [user.id] : []; + } +} diff --git a/space/store/root.ts b/space/store/root.ts index 6b87020ef7a..22b951d2070 100644 --- a/space/store/root.ts +++ b/space/store/root.ts @@ -5,6 +5,7 @@ import UserStore from "./user"; import IssueStore, { IIssueStore } from "./issue"; import ProjectStore, { IProjectStore } from "./project"; import IssueDetailStore, { IIssueDetailStore } from "./issue_details"; +import { IMentionsStore, MentionsStore } from "./mentions.store"; enableStaticRendering(typeof window === "undefined"); @@ -13,11 +14,13 @@ export class RootStore { issue: IIssueStore; issueDetails: IIssueDetailStore; project: IProjectStore; + mentionsStore: IMentionsStore; constructor() { this.user = new UserStore(this); this.issue = new IssueStore(this); this.project = new ProjectStore(this); this.issueDetails = new IssueDetailStore(this); + this.mentionsStore = new MentionsStore(this); } } diff --git a/space/store/user.ts b/space/store/user.ts index cec2d340fdc..e2b6428ef94 100644 --- a/space/store/user.ts +++ b/space/store/user.ts @@ -2,7 +2,6 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"; // service import UserService from "services/user.service"; -import { ActorDetail } from "types/issue"; // types import { IUser } from "types/user"; diff --git a/space/styles/globals.css b/space/styles/globals.css index 1782b9b810f..ea04bcda602 100644 --- a/space/styles/globals.css +++ b/space/styles/globals.css @@ -199,9 +199,9 @@ --color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */ --color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */ - --color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */ - --color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */ - --color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */ + --color-sidebar-border-200: var(--color-border-200); /* subtle sidebar border- 2 */ + --color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */ + --color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */ } } diff --git a/space/styles/table.css b/space/styles/table.css index ad88fd10ec8..8a47a8c59fd 100644 --- a/space/styles/table.css +++ b/space/styles/table.css @@ -92,7 +92,7 @@ transform: translateY(-50%); } -.tableWrapper .tableControls .columnsControl > button { +.tableWrapper .tableControls .columnsControl .columnsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); width: 30px; @@ -104,26 +104,42 @@ transform: translateX(-50%); } -.tableWrapper .tableControls .rowsControl > button { +.tableWrapper .tableControls .rowsControl .rowsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); height: 30px; width: 15px; } -.tableWrapper .tableControls button { +.tableWrapper .tableControls .rowsControlDiv { background-color: rgba(var(--color-primary-100)); border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; background-size: 1.25rem; background-repeat: no-repeat; background-position: center; - transition: transform ease-out 100ms, background-color ease-out 100ms; + transition: + transform ease-out 100ms, + background-color ease-out 100ms; outline: none; box-shadow: #000 0px 2px 4px; cursor: pointer; } +.tableWrapper .tableControls .columnsControlDiv { + background-color: rgba(var(--color-primary-100)); + border: 1px solid rgba(var(--color-border-200)); + border-radius: 2px; + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: center; + transition: + transform ease-out 100ms, + background-color ease-out 100ms; + outline: none; + box-shadow: #000 0px 2px 4px; + cursor: pointer; +} .tableWrapper .tableControls .tableToolbox, .tableWrapper .tableControls .tableColorPickerToolbox { border: 1px solid rgba(var(--color-border-300)); diff --git a/space/types/issue.ts b/space/types/issue.ts index 206327fcdac..4b76c75e8d9 100644 --- a/space/types/issue.ts +++ b/space/types/issue.ts @@ -24,7 +24,6 @@ export interface IIssueGroup { title: TIssueGroupTitle; color: string; className: string; - icon: React.FC; } export interface IIssue { @@ -40,7 +39,12 @@ export interface IIssue { sequence_id: number; start_date: any; state: string; - state_detail: any; + state_detail: { + id: string; + name: string; + group: TIssueGroupKey; + color: string; + }; target_date: any; votes: IVote[]; } diff --git a/turbo.json b/turbo.json index 62afa90bb16..ac462d08bae 100644 --- a/turbo.json +++ b/turbo.json @@ -5,7 +5,6 @@ "NEXT_PUBLIC_DEPLOY_URL", "NEXT_PUBLIC_SENTRY_DSN", "NEXT_PUBLIC_SENTRY_ENVIRONMENT", - "NEXT_PUBLIC_GITHUB_APP_NAME", "NEXT_PUBLIC_ENABLE_SENTRY", "NEXT_PUBLIC_ENABLE_OAUTH", "NEXT_PUBLIC_TRACK_EVENTS", diff --git a/web/components/account/github-login-button.tsx b/web/components/account/github-login-button.tsx index 9ea5b7df2a4..fc140f632ce 100644 --- a/web/components/account/github-login-button.tsx +++ b/web/components/account/github-login-button.tsx @@ -32,8 +32,7 @@ export const GithubLoginButton: FC = (props) => { }, [code, gitCode, handleSignIn]); useEffect(() => { - const origin = - typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; setLoginCallBackURL(`${origin}/` as any); }, []); diff --git a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx index e58d816661a..3544ae46d3b 100644 --- a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx +++ b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx @@ -49,10 +49,7 @@ export const CustomTooltip: React.FC = ({ datum, analytics, params }) => : "" }`} > - {params.segment === "assignees__id" - ? renderAssigneeName(tooltipValue.toString()) - : tooltipValue} - : + {params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}: {datum.value}
diff --git a/web/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx index 181eec8bd9d..0908609c9a1 100644 --- a/web/components/analytics/custom-analytics/graph/index.tsx +++ b/web/components/analytics/custom-analytics/graph/index.tsx @@ -112,8 +112,9 @@ export const AnalyticsGraph: React.FC = ({ analytics, barGraphData, param 7 ? "end" : "middle"}`} fontSize={10} + fill="rgb(var(--color-text-200))" className={`${barGraphData.data.length > 7 ? "-rotate-45" : ""}`} > {generateDisplayName(datum.value, analytics, params, "x_axis")} diff --git a/web/components/analytics/scope-and-demand/scope.tsx b/web/components/analytics/scope-and-demand/scope.tsx index 9231947bd35..4c69a23c5f8 100644 --- a/web/components/analytics/scope-and-demand/scope.tsx +++ b/web/components/analytics/scope-and-demand/scope.tsx @@ -22,9 +22,7 @@ export const AnalyticsScope: React.FC = ({ defaultAnalytics }) => ( keys={["count"]} height="250px" colors={() => `#f97316`} - customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => - d.count > 0 ? d.count : 50 - )} + customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => (d.count > 0 ? d.count : 50))} tooltip={(datum) => { const assignee = defaultAnalytics.pending_issue_user.find( (a) => a.assignees__id === `${datum.indexValue}` diff --git a/web/components/auth-screens/not-authorized-view.tsx b/web/components/auth-screens/not-authorized-view.tsx index 32d96eff2a8..509bc1e8428 100644 --- a/web/components/auth-screens/not-authorized-view.tsx +++ b/web/components/auth-screens/not-authorized-view.tsx @@ -31,9 +31,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { alt="ProjectSettingImg" />
-

- Oops! You are not authorized to view this page -

+

Oops! You are not authorized to view this page

{user ? ( diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index 3e664bef4a4..1083073daeb 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -1,7 +1,9 @@ import React, { useState } from "react"; - +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // component -import { CustomSelect, ToggleSwitch } from "@plane/ui"; +import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui"; import { SelectMonthModal } from "components/automation"; // icon import { ArchiveRestore } from "lucide-react"; @@ -11,15 +13,21 @@ import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; import { IProject } from "types"; type Props = { - projectDetails: IProject | undefined; handleChange: (formData: Partial) => Promise; - disabled?: boolean; }; -export const AutoArchiveAutomation: React.FC = ({ projectDetails, handleChange, disabled = false }) => { +const initialValues: Partial = { archive_in: 1 }; + +export const AutoArchiveAutomation: React.FC = observer((props) => { + const { handleChange } = props; + // states const [monthModal, setmonthModal] = useState(false); - const initialValues: Partial = { archive_in: 1 }; + const { user: userStore, project: projectStore } = useMobxStore(); + + const projectDetails = projectStore.currentProjectDetails; + const userRole = userStore.currentProjectRole; + return ( <> = ({ projectDetails, handleC handleClose={() => setmonthModal(false)} handleChange={handleChange} /> -
+
@@ -48,46 +56,52 @@ export const AutoArchiveAutomation: React.FC = ({ projectDetails, handleC projectDetails?.archive_in === 0 ? handleChange({ archive_in: 1 }) : handleChange({ archive_in: 0 }) } size="sm" - disabled={disabled} + disabled={userRole !== 20} />
- {projectDetails?.archive_in !== 0 && ( -
-
-
Auto-archive issues that are closed for
-
- { - handleChange({ archive_in: val }); - }} - input - width="w-full" - disabled={disabled} - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} + {projectDetails ? ( + projectDetails.archive_in !== 0 && ( +
+
+
Auto-archive issues that are closed for
+
+ { + handleChange({ archive_in: val }); + }} + input + width="w-full" + disabled={userRole !== 20} + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} - - - + + + +
-
+ ) + ) : ( + + + )}
); -}; +}); diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index 7da250295aa..1f0ef1c31b2 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -1,42 +1,32 @@ import React, { useState } from "react"; -import useSWR from "swr"; -import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // component import { SelectMonthModal } from "components/automation"; -import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon } from "@plane/ui"; +import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui"; // icons import { ArchiveX } from "lucide-react"; -// services -import { ProjectStateService } from "services/project"; -// constants -import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; -import { STATES_LIST } from "constants/fetch-keys"; // types import { IProject } from "types"; -// helper -import { getStatesList } from "helpers/state.helper"; +// fetch keys +import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; type Props = { - projectDetails: IProject | undefined; handleChange: (formData: Partial) => Promise; - disabled?: boolean; }; -const projectStateService = new ProjectStateService(); - -export const AutoCloseAutomation: React.FC = ({ projectDetails, handleChange, disabled = false }) => { +export const AutoCloseAutomation: React.FC = observer((props) => { + const { handleChange } = props; + // states const [monthModal, setmonthModal] = useState(false); - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { user: userStore, project: projectStore, projectState: projectStateStore } = useMobxStore(); - const { data: stateGroups } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => projectStateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - const states = getStatesList(stateGroups); + const userRole = userStore.currentProjectRole; + const projectDetails = projectStore.currentProjectDetails; + // const stateGroups = projectStateStore.groupedProjectStates ?? undefined; + const states = projectStateStore.projectStates; const options = states ?.filter((state) => state.group === "cancelled") @@ -53,7 +43,7 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha const multipleOptions = (options ?? []).length > 1; - const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null; + const defaultState = states?.find((s) => s.group === "cancelled")?.id || null; const selectedOption = states?.find((s) => s.id === projectDetails?.default_state ?? defaultState); const currentDefaultState = states?.find((s) => s.id === defaultState); @@ -72,7 +62,6 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha handleClose={() => setmonthModal(false)} handleChange={handleChange} /> -
@@ -82,7 +71,7 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha

Auto-close issues

- Plane will automatically close issue that haven’t been completed or cancelled. + Plane will automatically close issue that haven{"'"}t been completed or cancelled.

@@ -94,87 +83,93 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha : handleChange({ close_in: 0, default_state: null }) } size="sm" - disabled={disabled} + disabled={userRole !== 20} />
- {projectDetails?.close_in !== 0 && ( -
-
-
-
Auto-close issues that are inactive for
-
- { - handleChange({ close_in: val }); - }} - input - width="w-full" - disabled={disabled} - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} - - - + {projectDetails ? ( + projectDetails.close_in !== 0 && ( +
+
+
+
Auto-close issues that are inactive for
+
+ { + handleChange({ close_in: val }); + }} + input + width="w-full" + disabled={userRole !== 20} + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} + + + +
-
-
-
Auto-close Status
-
- - {selectedOption ? ( - - ) : currentDefaultState ? ( - - ) : ( - - )} - {selectedOption?.name - ? selectedOption.name - : currentDefaultState?.name ?? State} -
- } - onChange={(val: string) => { - handleChange({ default_state: val }); - }} - options={options} - disabled={!multipleOptions} - width="w-full" - input - /> +
+
Auto-close Status
+
+ + {selectedOption ? ( + + ) : currentDefaultState ? ( + + ) : ( + + )} + {selectedOption?.name + ? selectedOption.name + : currentDefaultState?.name ?? State} +
+ } + onChange={(val: string) => { + handleChange({ default_state: val }); + }} + options={options} + disabled={!multipleOptions} + width="w-full" + input + /> +
-
+ ) + ) : ( + + + )}
); -}; +}); diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index 2d65bd58ad4..3acfb71a63e 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; import { Command } from "cmdk"; import { Dialog, Transition } from "@headlessui/react"; +import { observer } from "mobx-react-lite"; import { FileText, FolderPlus, @@ -16,12 +17,13 @@ import { UserMinus2, UserPlus2, } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // services import { WorkspaceService } from "services/workspace.service"; import { IssueService } from "services/issue"; // hooks import useDebounce from "hooks/use-debounce"; -import useUser from "hooks/use-user"; import useToast from "hooks/use-toast"; // components import { @@ -61,11 +63,8 @@ type Props = { const workspaceService = new WorkspaceService(); const issueService = new IssueService(); -export const CommandModal: React.FC = (props) => { +export const CommandModal: React.FC = observer((props) => { const { deleteIssue, isPaletteOpen, closePalette } = props; - // router - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; // states const [placeholder, setPlaceholder] = useState("Type a command or search..."); const [resultsCount, setResultsCount] = useState(0); @@ -86,14 +85,19 @@ export const CommandModal: React.FC = (props) => { const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [pages, setPages] = useState([]); + const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore(); + const user = userStore.currentUser ?? undefined; + + // router + const router = useRouter(); + const { workspaceSlug, projectId, issueId } = router.query; + const page = pages[pages.length - 1]; const debouncedSearchTerm = useDebounce(searchTerm, 500); const { setToastAlert } = useToast(); - const { user } = useUser(); - const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId @@ -468,10 +472,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateIssueModal(true); }} className="focus:bg-custom-background-80" > @@ -488,10 +489,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateProjectModal(true); }} className="focus:outline-none" > @@ -510,10 +508,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "q", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateCycleModal(true); }} className="focus:outline-none" > @@ -528,10 +523,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "m", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateModuleModal(true); }} className="focus:outline-none" > @@ -546,10 +538,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "v", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreateViewModal(true); }} className="focus:outline-none" > @@ -564,10 +553,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "d", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleCreatePageModal(true); }} className="focus:outline-none" > @@ -621,10 +607,7 @@ export const CommandModal: React.FC = (props) => { { closePalette(); - const e = new KeyboardEvent("keydown", { - key: "h", - }); - document.dispatchEvent(e); + commandPaletteStore.toggleShortcutModal(true); }} className="focus:outline-none" > @@ -762,4 +745,4 @@ export const CommandModal: React.FC = (props) => { ); -}; +}); diff --git a/web/components/command-palette/issue/change-issue-assignee.tsx b/web/components/command-palette/issue/change-issue-assignee.tsx index 2d655571c83..512428310b4 100644 --- a/web/components/command-palette/issue/change-issue-assignee.tsx +++ b/web/components/command-palette/issue/change-issue-assignee.tsx @@ -26,15 +26,16 @@ const issueService = new IssueService(); export const ChangeIssueAssignee: FC = observer((props) => { const { setIsPaletteOpen, issue, user } = props; - + // router const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; - - const { project: projectStore } = useMobxStore(); - const members = projectId ? projectStore.members?.[projectId.toString()] : undefined; + // store + const { + projectMember: { projectMembers }, + } = useMobxStore(); const options = - members?.map(({ member }) => ({ + projectMembers?.map(({ member }) => ({ value: member.id, query: member.display_name, content: ( diff --git a/web/components/command-palette/issue/change-issue-state.tsx b/web/components/command-palette/issue/change-issue-state.tsx index 03f495603ba..688aeb49f8a 100644 --- a/web/components/command-palette/issue/change-issue-state.tsx +++ b/web/components/command-palette/issue/change-issue-state.tsx @@ -1,9 +1,6 @@ import React, { Dispatch, SetStateAction, useCallback } from "react"; - import { useRouter } from "next/router"; - import useSWR, { mutate } from "swr"; - // cmdk import { Command } from "cmdk"; // services @@ -13,8 +10,6 @@ import { ProjectStateService } from "services/project"; import { Spinner, StateGroupIcon } from "@plane/ui"; // icons import { Check } from "lucide-react"; -// helpers -import { getStatesList } from "helpers/state.helper"; // types import { IUser, IIssue } from "types"; // fetch keys @@ -34,11 +29,10 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue, use const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; - const { data: stateGroups, mutate: mutateIssueDetails } = useSWR( + const { data: states, mutate: mutateStates } = useSWR( workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null ); - const states = getStatesList(stateGroups); const submitChanges = useCallback( async (formData: Partial) => { @@ -60,14 +54,14 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue, use await issueService .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { - mutateIssueDetails(); + mutateStates(); mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); }) .catch((e) => { console.error(e); }); }, - [workspaceSlug, issueId, projectId, mutateIssueDetails, user] + [workspaceSlug, issueId, projectId, mutateStates, user] ); const handleIssueState = (stateId: string) => { diff --git a/web/components/command-palette/shortcuts-modal.tsx b/web/components/command-palette/shortcuts-modal.tsx index 85d2b4fe4ca..3c7c199699e 100644 --- a/web/components/command-palette/shortcuts-modal.tsx +++ b/web/components/command-palette/shortcuts-modal.tsx @@ -1,5 +1,4 @@ -import { FC, useEffect, useState, Dispatch, SetStateAction, Fragment } from "react"; -// headless ui +import { FC, useEffect, useState, Fragment } from "react"; import { Dialog, Transition } from "@headlessui/react"; // icons import { Command, Search, X } from "lucide-react"; diff --git a/web/components/core/sidebar/progress-chart.tsx b/web/components/core/sidebar/progress-chart.tsx index cfa28c5e159..8108b87afac 100644 --- a/web/components/core/sidebar/progress-chart.tsx +++ b/web/components/core/sidebar/progress-chart.tsx @@ -57,8 +57,7 @@ const ProgressChart: React.FC = ({ distribution, startDate, endDate, tota const interval = Math.ceil(totalDates / maxDates); const limitedDates = []; - for (let i = 0; i < totalDates; i += interval) - limitedDates.push(renderShortNumericDateFormat(dates[i])); + for (let i = 0; i < totalDates; i += interval) limitedDates.push(renderShortNumericDateFormat(dates[i])); if (!limitedDates.includes(renderShortNumericDateFormat(dates[totalDates - 1]))) limitedDates.push(renderShortNumericDateFormat(dates[totalDates - 1])); diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index fc89ac3fbe8..ea7ffa7481f 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -1,10 +1,12 @@ import { MouseEvent } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; import useSWR from "swr"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; // ui import { SingleProgressStats } from "components/core"; import { @@ -25,7 +27,6 @@ import { ActiveCycleProgressStats } from "components/cycles"; import { ViewIssueLabel } from "components/issues"; // icons import { AlarmClock, AlertTriangle, ArrowRight, CalendarDays, Star, Target } from "lucide-react"; - // helpers import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; @@ -65,21 +66,21 @@ interface IActiveCycleDetails { projectId: string; } -export const ActiveCycleDetails: React.FC = (props) => { +export const ActiveCycleDetails: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId } = props; - const { cycle: cycleStore } = useMobxStore(); + const { cycle: cycleStore, commandPalette: commandPaletteStore } = useMobxStore(); const { setToastAlert } = useToast(); - const { isLoading } = useSWR( + useSWR( workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null ); - const activeCycle = cycleStore.cycles?.[projectId] || null; + const activeCycle = cycleStore.cycles?.[projectId]?.active || null; const cycle = activeCycle ? activeCycle[0] : null; const issues = (cycleStore?.active_cycle_issues as any) || null; @@ -93,7 +94,7 @@ export const ActiveCycleDetails: React.FC = (props) => { // : null // ) as { data: IIssue[] | undefined }; - if (isLoading) + if (!cycle) return ( @@ -117,12 +118,7 @@ export const ActiveCycleDetails: React.FC = (props) => { @@ -147,7 +143,7 @@ export const ActiveCycleDetails: React.FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { + cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -160,7 +156,7 @@ export const ActiveCycleDetails: React.FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { + cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -485,4 +481,4 @@ export const ActiveCycleDetails: React.FC = (props) => {
); -}; +}); diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index fb30150ca25..f1e0cf08412 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -41,7 +41,7 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa {peekCycle && (
= (props) => { const isCompleted = cycleStatus === "completed"; const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); + const isDateValid = cycle.start_date || cycle.end_date; const router = useRouter(); @@ -64,9 +65,7 @@ export const CyclesBoardCard: FC = (props) => { ? cycleTotalIssues === 0 ? "0 Issue" : cycleTotalIssues === cycle.completed_issues - ? cycleTotalIssues > 1 - ? `${cycleTotalIssues} Issues` - : `${cycleTotalIssues} Issue` + ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` : `${cycle.completed_issues}/${cycleTotalIssues} Issues` : "0 Issue"; @@ -88,7 +87,7 @@ export const CyclesBoardCard: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { + cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -101,7 +100,7 @@ export const CyclesBoardCard: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { + cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -225,10 +224,14 @@ export const CyclesBoardCard: FC = (props) => {
- - {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} -{" "} - {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} - + {isDateValid ? ( + + {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} -{" "} + {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + + ) : ( + No due date + )}
{cycle.is_favorite ? ( @@ -67,4 +67,4 @@ export const CyclesBoard: FC = (props) => { )} ); -}; +}); diff --git a/web/components/cycles/cycles-gantt.tsx b/web/components/cycles/cycles-gantt.tsx deleted file mode 100644 index 7b370ba3074..00000000000 --- a/web/components/cycles/cycles-gantt.tsx +++ /dev/null @@ -1 +0,0 @@ -export const CycleGantt = () => <>; diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index c294a5119ef..f93e428e74c 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -87,7 +87,7 @@ export const CyclesListItem: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { + cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -100,7 +100,7 @@ export const CyclesListItem: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => { + cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -155,9 +155,11 @@ export const CyclesListItem: FC = (props) => { {isCompleted ? ( - {`!`} - ) : progress === 100 ? ( - + progress === 100 ? ( + + ) : ( + {`!`} + ) ) : ( {`${progress}%`} )} @@ -229,7 +231,7 @@ export const CyclesListItem: FC = (props) => { )} - + {!isCompleted && ( <> @@ -241,7 +243,7 @@ export const CyclesListItem: FC = (props) => { - Delete module + Delete cycle diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 03698f1d81b..0cff682afd5 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,7 +1,9 @@ import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { CyclePeekOverview, CyclesListItem } from "components/cycles"; - // ui import { Loader } from "@plane/ui"; // types @@ -14,9 +16,11 @@ export interface ICyclesList { projectId: string; } -export const CyclesList: FC = (props) => { +export const CyclesList: FC = observer((props) => { const { cycles, filter, workspaceSlug, projectId } = props; + const { commandPalette: commandPaletteStore } = useMobxStore(); + return ( <> {cycles ? ( @@ -53,12 +57,7 @@ export const CyclesList: FC = (props) => { @@ -75,4 +74,4 @@ export const CyclesList: FC = (props) => { )} ); -}; +}); diff --git a/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx index 4eea43e6d0a..a4522f711c1 100644 --- a/web/components/cycles/cycles-view.tsx +++ b/web/components/cycles/cycles-view.tsx @@ -15,7 +15,7 @@ export interface ICyclesView { layout: TCycleLayout; workspaceSlug: string; projectId: string; - peekCycle: string; + peekCycle: string | undefined; } export const CyclesView: FC = observer((props) => { @@ -30,7 +30,14 @@ export const CyclesView: FC = observer((props) => { workspaceSlug && projectId && filter ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null ); - const cyclesList = cycleStore.cycles?.[projectId]; + const cyclesList = + filter === "completed" + ? cycleStore.projectCompletedCycles + : filter === "draft" + ? cycleStore.projectDraftCycles + : filter === "upcoming" + ? cycleStore.projectUpcomingCycles + : cycleStore.projectCycles; return ( <> diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index b262edb1519..7189b2fea76 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -135,7 +135,7 @@ export const CycleForm: React.FC = (props) => {
-
+
diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 9526de59c89..d5bd4e14035 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -10,7 +10,7 @@ import { CycleService } from "services/cycle.service"; import useUser from "hooks/use-user"; import useProjectDetails from "hooks/use-project-details"; // components -import { GanttChartRoot, IBlockUpdateData } from "components/gantt-chart"; +import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; import { CycleGanttBlock, CycleGanttSidebarBlock } from "components/cycles"; // types import { ICycle } from "types"; @@ -85,8 +85,8 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => loaderTitle="Cycles" blocks={cycles ? blockFormat(cycles) : null} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} + sidebarToRender={(props) => } blockToRender={(data: ICycle) => } - sidebarBlockToRender={(data: ICycle) => } enableBlockLeftResize={false} enableBlockRightResize={false} enableBlockMove={false} diff --git a/web/components/cycles/index.ts b/web/components/cycles/index.ts index ea956847875..db5e9de9eea 100644 --- a/web/components/cycles/index.ts +++ b/web/components/cycles/index.ts @@ -5,7 +5,6 @@ export * from "./gantt-chart"; export * from "./cycles-view"; export * from "./form"; export * from "./modal"; -export * from "./select"; export * from "./sidebar"; export * from "./transfer-issues-modal"; export * from "./transfer-issues"; @@ -13,7 +12,6 @@ export * from "./cycles-list"; export * from "./cycles-list-item"; export * from "./cycles-board"; export * from "./cycles-board-card"; -export * from "./cycles-gantt"; export * from "./delete-modal"; export * from "./cycle-peek-overview"; export * from "./cycles-list-item"; diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index bb266e9eb25..d941e5e4b51 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -50,7 +50,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { const updateCycle = async (cycleId: string, payload: Partial) => cycleStore - .updateCycle(workspaceSlug, projectId, cycleId, payload) + .patchCycle(workspaceSlug, projectId, cycleId, payload) .then(() => { setToastAlert({ type: "success", diff --git a/web/components/cycles/select.tsx b/web/components/cycles/select.tsx deleted file mode 100644 index 9944a39b2aa..00000000000 --- a/web/components/cycles/select.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; -import { Listbox, Transition } from "@headlessui/react"; -// icons -import { ContrastIcon } from "@plane/ui"; -import { Plus } from "lucide-react"; -// services -import { CycleService } from "services/cycle.service"; -// components -import { CycleCreateUpdateModal } from "components/cycles"; -// fetch-keys -import { CYCLES_LIST } from "constants/fetch-keys"; - -export type IssueCycleSelectProps = { - projectId: string; - value: any; - onChange: (value: any) => void; - multiple?: boolean; -}; - -const cycleService = new CycleService(); - -export const CycleSelect: React.FC = ({ projectId, value, onChange, multiple = false }) => { - // states - const [isCycleModalActive, setCycleModalActive] = useState(false); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { data: cycles } = useSWR( - workspaceSlug && projectId ? CYCLES_LIST(projectId) : null, - workspaceSlug && projectId - ? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "all") - : null - ); - - const options = cycles?.map((cycle) => ({ value: cycle.id, display: cycle.name })); - - const openCycleModal = () => { - setCycleModalActive(true); - }; - - const closeCycleModal = () => { - setCycleModalActive(false); - }; - - return ( - <> - {workspaceSlug && projectId && ( - - )} - - {({ open }) => ( - <> - - -
- {cycles?.find((c) => c.id === value)?.name ?? "Cycles"} -
-
- - - -
- {options ? ( - options.length > 0 ? ( - options.map((option) => ( - - `${ - selected || (Array.isArray(value) ? value.includes(option.value) : value === option.value) - ? "bg-indigo-50 font-medium" - : "" - } ${ - active ? "bg-indigo-50" : "" - } relative cursor-pointer select-none p-2 text-custom-text-100` - } - value={option.value} - > - {option.display} - - )) - ) : ( -

No options

- ) - ) : ( -

Loading...

- )} - -
-
-
- - )} -
- - ); -}; diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index ea154d48b09..3288dfe0b65 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -317,11 +317,11 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { {!isCompleted && ( - + setCycleDeleteModal(true)}> - - Delete + + Delete cycle @@ -502,7 +502,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
= observer((props) => { ) : ( "" )} - {cycleDetails.total_issues > 0 && ( + {cycleDetails.total_issues > 0 && cycleDetails.distribution && (
= observer((props) => { return ( <> -
+
diff --git a/web/components/estimates/estimate-list.tsx b/web/components/estimates/estimate-list.tsx index 4895450987b..07770b18327 100644 --- a/web/components/estimates/estimate-list.tsx +++ b/web/components/estimates/estimate-list.tsx @@ -71,7 +71,7 @@ export const EstimatesList: React.FC = observer(() => { data={projectStore.getProjectEstimateById(estimateToDelete!)} /> -
+

Estimates

diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index bb64e6170a1..2d7eb9d65dd 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -53,7 +53,7 @@ const IntegrationGuide = () => { {EXPORTERS_LIST.map((service) => (
@@ -79,7 +79,7 @@ const IntegrationGuide = () => { ))}
-
+

Previous Exports

diff --git a/web/components/gantt-chart/chart/index.tsx b/web/components/gantt-chart/chart/index.tsx index 219f11ebb45..c6df2945282 100644 --- a/web/components/gantt-chart/chart/index.tsx +++ b/web/components/gantt-chart/chart/index.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState } from "react"; // icons // components import { GanttChartBlocks } from "components/gantt-chart"; -import { GanttSidebar } from "../sidebar"; +// import { GanttSidebar } from "../sidebar"; // import { HourChartView } from "./hours"; // import { DayChartView } from "./day"; // import { WeekChartView } from "./week"; @@ -40,7 +40,7 @@ type ChartViewRootProps = { blocks: IGanttBlock[] | null; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockToRender: (data: any) => React.ReactNode; - sidebarBlockToRender: (block: any) => React.ReactNode; + sidebarToRender: (props: any) => React.ReactNode; enableBlockLeftResize: boolean; enableBlockRightResize: boolean; enableBlockMove: boolean; @@ -54,7 +54,7 @@ export const ChartViewRoot: FC = ({ blocks = null, loaderTitle, blockUpdateHandler, - sidebarBlockToRender, + sidebarToRender, blockToRender, enableBlockLeftResize, enableBlockRightResize, @@ -285,13 +285,8 @@ export const ChartViewRoot: FC = ({
{title}
Duration
- + + {sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
= () => { style={{ width: `${currentViewData?.data.width}px` }} >
- - {monthDay.dayData.shortTitle[0]} - {" "} - + {monthDay.dayData.shortTitle[0]}{" "} + {monthDay.day}
@@ -63,9 +55,7 @@ export const MonthChartView: FC = () => { >
{/* {monthDay?.today && ( diff --git a/web/components/gantt-chart/data/index.ts b/web/components/gantt-chart/data/index.ts index 4e1921434ad..58ac6e4b2a5 100644 --- a/web/components/gantt-chart/data/index.ts +++ b/web/components/gantt-chart/data/index.ts @@ -27,8 +27,7 @@ export const months: WeekMonthDataType[] = [ { key: 11, shortTitle: "dec", title: "december" }, ]; -export const charCapitalize = (word: string) => - `${word.charAt(0).toUpperCase()}${word.substring(1)}`; +export const charCapitalize = (word: string) => `${word.charAt(0).toUpperCase()}${word.substring(1)}`; export const bindZero = (value: number) => (value > 9 ? `${value}` : `0${value}`); @@ -50,9 +49,7 @@ export const datePreview = (date: Date, includeTime: boolean = false) => { month = months[month as number] as WeekMonthDataType; const year = date.getFullYear(); - return `${charCapitalize(month?.shortTitle)} ${day}, ${year}${ - includeTime ? `, ${timePreview(date)}` : `` - }`; + return `${charCapitalize(month?.shortTitle)} ${day}, ${year}${includeTime ? `, ${timePreview(date)}` : ``}`; }; // context data @@ -137,8 +134,6 @@ export const allViewsWithData: ChartDataType[] = [ ]; export const currentViewDataWithView = (view: string = "month") => { - const currentView: ChartDataType | undefined = allViewsWithData.find( - (_viewData) => _viewData.key === view - ); + const currentView: ChartDataType | undefined = allViewsWithData.find((_viewData) => _viewData.key === view); return currentView; }; diff --git a/web/components/gantt-chart/index.ts b/web/components/gantt-chart/index.ts index 4520ee194a1..ead6960868b 100644 --- a/web/components/gantt-chart/index.ts +++ b/web/components/gantt-chart/index.ts @@ -3,3 +3,4 @@ export * from "./helpers"; export * from "./hooks"; export * from "./root"; export * from "./types"; +export * from "./sidebar"; diff --git a/web/components/gantt-chart/root.tsx b/web/components/gantt-chart/root.tsx index 7c1f243d6ff..10c00a36322 100644 --- a/web/components/gantt-chart/root.tsx +++ b/web/components/gantt-chart/root.tsx @@ -13,7 +13,7 @@ type GanttChartRootProps = { blocks: IGanttBlock[] | null; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockToRender: (data: any) => React.ReactNode; - sidebarBlockToRender: (block: any) => React.ReactNode; + sidebarToRender: (props: any) => React.ReactNode; enableBlockLeftResize?: boolean; enableBlockRightResize?: boolean; enableBlockMove?: boolean; @@ -27,7 +27,7 @@ export const GanttChartRoot: FC = ({ blocks, loaderTitle = "blocks", blockUpdateHandler, - sidebarBlockToRender, + sidebarToRender, blockToRender, enableBlockLeftResize = true, enableBlockRightResize = true, @@ -42,7 +42,7 @@ export const GanttChartRoot: FC = ({ blocks={blocks} loaderTitle={loaderTitle} blockUpdateHandler={blockUpdateHandler} - sidebarBlockToRender={sidebarBlockToRender} + sidebarToRender={sidebarToRender} blockToRender={blockToRender} enableBlockLeftResize={enableBlockLeftResize} enableBlockRightResize={enableBlockRightResize} diff --git a/web/components/gantt-chart/cycle-sidebar.tsx b/web/components/gantt-chart/sidebar/cycle-sidebar.tsx similarity index 93% rename from web/components/gantt-chart/cycle-sidebar.tsx rename to web/components/gantt-chart/sidebar/cycle-sidebar.tsx index 5ecc1f3ba0d..0103c745924 100644 --- a/web/components/gantt-chart/cycle-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/cycle-sidebar.tsx @@ -3,25 +3,26 @@ import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import { MoreVertical } from "lucide-react"; // hooks -import { useChart } from "./hooks"; +import { useChart } from "components/gantt-chart/hooks"; // ui import { Loader } from "@plane/ui"; +// components +import { CycleGanttSidebarBlock } from "components/cycles"; // helpers import { findTotalDaysInRange } from "helpers/date-time.helper"; // types -import { IBlockUpdateData, IGanttBlock } from "./types"; +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; type Props = { title: string; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blocks: IGanttBlock[] | null; - SidebarBlockRender: React.FC; enableReorder: boolean; }; -export const GanttSidebar: React.FC = (props) => { +export const CycleGanttSidebar: React.FC = (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props; + const { title, blockUpdateHandler, blocks, enableReorder } = props; const router = useRouter(); const { cycleId } = router.query; @@ -128,7 +129,7 @@ export const GanttSidebar: React.FC = (props) => { )}
- +
{duration} day{duration > 1 ? "s" : ""} diff --git a/web/components/gantt-chart/sidebar/index.ts b/web/components/gantt-chart/sidebar/index.ts new file mode 100644 index 00000000000..8b1d7223725 --- /dev/null +++ b/web/components/gantt-chart/sidebar/index.ts @@ -0,0 +1,4 @@ +export * from "./cycle-sidebar"; +export * from "./module-sidebar"; +export * from "./sidebar"; +export * from "./project-view-sidebar"; diff --git a/web/components/gantt-chart/module-sidebar.tsx b/web/components/gantt-chart/sidebar/module-sidebar.tsx similarity index 93% rename from web/components/gantt-chart/module-sidebar.tsx rename to web/components/gantt-chart/sidebar/module-sidebar.tsx index 5ecc1f3ba0d..8e076d46330 100644 --- a/web/components/gantt-chart/module-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/module-sidebar.tsx @@ -3,25 +3,26 @@ import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import { MoreVertical } from "lucide-react"; // hooks -import { useChart } from "./hooks"; +import { useChart } from "components/gantt-chart/hooks"; // ui import { Loader } from "@plane/ui"; +// components +import { ModuleGanttSidebarBlock } from "components/modules"; // helpers import { findTotalDaysInRange } from "helpers/date-time.helper"; // types -import { IBlockUpdateData, IGanttBlock } from "./types"; +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; type Props = { title: string; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blocks: IGanttBlock[] | null; - SidebarBlockRender: React.FC; enableReorder: boolean; }; -export const GanttSidebar: React.FC = (props) => { +export const ModuleGanttSidebar: React.FC = (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props; + const { title, blockUpdateHandler, blocks, enableReorder } = props; const router = useRouter(); const { cycleId } = router.query; @@ -128,7 +129,7 @@ export const GanttSidebar: React.FC = (props) => { )}
- +
{duration} day{duration > 1 ? "s" : ""} diff --git a/web/components/gantt-chart/sidebar/project-view-sidebar.tsx b/web/components/gantt-chart/sidebar/project-view-sidebar.tsx new file mode 100644 index 00000000000..c327e1eac59 --- /dev/null +++ b/web/components/gantt-chart/sidebar/project-view-sidebar.tsx @@ -0,0 +1,160 @@ +import { useRouter } from "next/router"; +import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd"; +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { MoreVertical } from "lucide-react"; +// hooks +import { useChart } from "components/gantt-chart/hooks"; +// ui +import { Loader } from "@plane/ui"; +// components +import { IssueGanttSidebarBlock } from "components/issues"; +// helpers +import { findTotalDaysInRange } from "helpers/date-time.helper"; +// types +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; + +type Props = { + title: string; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blocks: IGanttBlock[] | null; + enableReorder: boolean; + enableQuickIssueCreate?: boolean; +}; + +export const ProjectViewGanttSidebar: React.FC = (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { title, blockUpdateHandler, blocks, enableReorder } = props; + + const router = useRouter(); + const { cycleId } = router.query; + + const { activeBlock, dispatch } = useChart(); + + // update the active block on hover + const updateActiveBlock = (block: IGanttBlock | null) => { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + activeBlock: block, + }, + }); + }; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.map((block, index) => { + const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? "", true); + + return ( + + {(provided, snapshot) => ( +
updateActiveBlock(block)} + onMouseLeave={() => updateActiveBlock(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+
+ +
+
+ {duration} day{duration > 1 ? "s" : ""} +
+
+
+
+ )} +
+ ); + }) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/sidebar.tsx b/web/components/gantt-chart/sidebar/sidebar.tsx similarity index 90% rename from web/components/gantt-chart/sidebar.tsx rename to web/components/gantt-chart/sidebar/sidebar.tsx index f6c32e0998e..72fbe126740 100644 --- a/web/components/gantt-chart/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/sidebar.tsx @@ -3,28 +3,27 @@ import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import { MoreVertical } from "lucide-react"; // hooks -import { useChart } from "./hooks"; +import { useChart } from "components/gantt-chart/hooks"; // ui import { Loader } from "@plane/ui"; // components -import { GanttInlineCreateIssueForm } from "components/issues"; +import { GanttInlineCreateIssueForm, IssueGanttSidebarBlock } from "components/issues"; // helpers import { findTotalDaysInRange } from "helpers/date-time.helper"; // types -import { IBlockUpdateData, IGanttBlock } from "./types"; +import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; type Props = { title: string; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blocks: IGanttBlock[] | null; - sidebarBlockToRender: (block: any) => React.ReactNode; enableReorder: boolean; enableQuickIssueCreate?: boolean; }; -export const GanttSidebar: React.FC = (props) => { +export const IssueGanttSidebar: React.FC = (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, sidebarBlockToRender, enableReorder, enableQuickIssueCreate } = props; + const { title, blockUpdateHandler, blocks, enableReorder, enableQuickIssueCreate } = props; const router = useRouter(); const { cycleId } = router.query; @@ -130,7 +129,9 @@ export const GanttSidebar: React.FC = (props) => { )}
-
{sidebarBlockToRender(block.data)}
+
+ +
{duration} day{duration > 1 ? "s" : ""}
@@ -151,7 +152,7 @@ export const GanttSidebar: React.FC = (props) => { )} {droppableProvided.placeholder} - + {enableQuickIssueCreate && }
)} diff --git a/web/components/gantt-chart/views/bi-week-view.ts b/web/components/gantt-chart/views/bi-week-view.ts index b8d8a0bfa51..14c0aad15e4 100644 --- a/web/components/gantt-chart/views/bi-week-view.ts +++ b/web/components/gantt-chart/views/bi-week-view.ts @@ -3,12 +3,7 @@ import { ChartDataType } from "../types"; // data import { weeks, months } from "../data"; // helpers -import { - generateDate, - getWeekNumberByDate, - getNumberOfDaysInMonth, - getDatesBetweenTwoDates, -} from "./helpers"; +import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; type GetAllDaysInMonthInMonthViewType = { date: any; @@ -34,9 +29,7 @@ const getAllDaysInMonthInMonthView = (month: number, year: number) => { title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, active: false, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -72,16 +65,8 @@ export const generateBiWeekChart = (monthPayload: ChartDataType, side: null | "l if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -96,16 +81,8 @@ export const generateBiWeekChart = (monthPayload: ChartDataType, side: null | "l } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -116,16 +93,8 @@ export const generateBiWeekChart = (monthPayload: ChartDataType, side: null | "l } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); diff --git a/web/components/gantt-chart/views/day-view.ts b/web/components/gantt-chart/views/day-view.ts index 246ecd8b8bd..0801b7bb187 100644 --- a/web/components/gantt-chart/views/day-view.ts +++ b/web/components/gantt-chart/views/day-view.ts @@ -10,8 +10,7 @@ export const getWeekNumberByDate = (date: Date) => { const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000; const weekStart = new Date(firstWeekStart); - const weekNumber = - Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; + const weekNumber = Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; return weekNumber; }; @@ -25,8 +24,7 @@ export const getNumberOfDaysInMonth = (month: number, year: number) => { return date.getDate(); }; -export const generateDate = (day: number, month: number, year: number) => - new Date(year, month, day); +export const generateDate = (day: number, month: number, year: number) => new Date(year, month, day); export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { const months = []; @@ -45,8 +43,7 @@ export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { months.push(new Date(currentYear, currentMonth)); currentDate.setMonth(currentDate.getMonth() + 1); } - if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) - months.push(endDate); + if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) months.push(endDate); return months; }; @@ -73,9 +70,7 @@ export const getAllDaysInMonth = (month: number, year: number) => { weekNumber: getWeekNumberByDate(date), title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -99,10 +94,7 @@ export const generateMonthDataByMonth = (month: number, year: number) => { return monthPayload; }; -export const generateMonthDataByYear = ( - monthPayload: ChartDataType, - side: null | "left" | "right" -) => { +export const generateMonthDataByYear = (monthPayload: ChartDataType, side: null | "left" | "right") => { let renderState = monthPayload; const renderPayload: any = []; @@ -114,16 +106,8 @@ export const generateMonthDataByYear = ( if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -138,16 +122,8 @@ export const generateMonthDataByYear = ( } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -158,16 +134,8 @@ export const generateMonthDataByYear = ( } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); diff --git a/web/components/gantt-chart/views/helpers.ts b/web/components/gantt-chart/views/helpers.ts index bfea6429747..94b614286ac 100644 --- a/web/components/gantt-chart/views/helpers.ts +++ b/web/components/gantt-chart/views/helpers.ts @@ -1,6 +1,5 @@ // Generating the date by using the year, month, and day -export const generateDate = (day: number, month: number, year: number) => - new Date(year, month, day); +export const generateDate = (day: number, month: number, year: number) => new Date(year, month, day); // Getting the number of days in a month export const getNumberOfDaysInMonth = (month: number, year: number) => { @@ -20,8 +19,7 @@ export const getWeekNumberByDate = (date: Date) => { const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000; const weekStart = new Date(firstWeekStart); - const weekNumber = - Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; + const weekNumber = Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; return weekNumber; }; @@ -86,8 +84,7 @@ export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { dates.push(new Date(currentYear, currentMonth)); currentDate.setMonth(currentDate.getMonth() + 1); } - if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) - dates.push(endDate); + if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) dates.push(endDate); return dates; }; diff --git a/web/components/gantt-chart/views/hours-view.ts b/web/components/gantt-chart/views/hours-view.ts index 246ecd8b8bd..0801b7bb187 100644 --- a/web/components/gantt-chart/views/hours-view.ts +++ b/web/components/gantt-chart/views/hours-view.ts @@ -10,8 +10,7 @@ export const getWeekNumberByDate = (date: Date) => { const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000; const weekStart = new Date(firstWeekStart); - const weekNumber = - Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; + const weekNumber = Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; return weekNumber; }; @@ -25,8 +24,7 @@ export const getNumberOfDaysInMonth = (month: number, year: number) => { return date.getDate(); }; -export const generateDate = (day: number, month: number, year: number) => - new Date(year, month, day); +export const generateDate = (day: number, month: number, year: number) => new Date(year, month, day); export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { const months = []; @@ -45,8 +43,7 @@ export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { months.push(new Date(currentYear, currentMonth)); currentDate.setMonth(currentDate.getMonth() + 1); } - if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) - months.push(endDate); + if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) months.push(endDate); return months; }; @@ -73,9 +70,7 @@ export const getAllDaysInMonth = (month: number, year: number) => { weekNumber: getWeekNumberByDate(date), title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -99,10 +94,7 @@ export const generateMonthDataByMonth = (month: number, year: number) => { return monthPayload; }; -export const generateMonthDataByYear = ( - monthPayload: ChartDataType, - side: null | "left" | "right" -) => { +export const generateMonthDataByYear = (monthPayload: ChartDataType, side: null | "left" | "right") => { let renderState = monthPayload; const renderPayload: any = []; @@ -114,16 +106,8 @@ export const generateMonthDataByYear = ( if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -138,16 +122,8 @@ export const generateMonthDataByYear = ( } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -158,16 +134,8 @@ export const generateMonthDataByYear = ( } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); diff --git a/web/components/gantt-chart/views/month-view.ts b/web/components/gantt-chart/views/month-view.ts index db21e372bf8..fc145d69c3b 100644 --- a/web/components/gantt-chart/views/month-view.ts +++ b/web/components/gantt-chart/views/month-view.ts @@ -3,12 +3,7 @@ import { ChartDataType, IGanttBlock } from "../types"; // data import { weeks, months } from "../data"; // helpers -import { - generateDate, - getWeekNumberByDate, - getNumberOfDaysInMonth, - getDatesBetweenTwoDates, -} from "./helpers"; +import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; type GetAllDaysInMonthInMonthViewType = { date: any; @@ -62,9 +57,7 @@ const getAllDaysInMonthInMonthView = (month: number, year: number): IMonthChild[ title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, active: false, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -100,16 +93,8 @@ export const generateMonthChart = (monthPayload: ChartDataType, side: null | "le if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -124,16 +109,8 @@ export const generateMonthChart = (monthPayload: ChartDataType, side: null | "le } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -144,16 +121,8 @@ export const generateMonthChart = (monthPayload: ChartDataType, side: null | "le } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -191,10 +160,7 @@ export const getNumberOfDaysBetweenTwoDatesInMonth = (startDate: Date, endDate: }; // calc item scroll position and width -export const getMonthChartItemPositionWidthInMonth = ( - chartData: ChartDataType, - itemData: IGanttBlock -) => { +export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, itemData: IGanttBlock) => { let scrollPosition: number = 0; let scrollWidth: number = 0; @@ -207,9 +173,7 @@ export const getMonthChartItemPositionWidthInMonth = ( // position code starts const positionTimeDifference: number = startDate.getTime() - itemStartDate.getTime(); - const positionDaysDifference: number = Math.abs( - Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24)) - ); + const positionDaysDifference: number = Math.abs(Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24))); scrollPosition = positionDaysDifference * chartData.data.width; var diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12; @@ -221,9 +185,7 @@ export const getMonthChartItemPositionWidthInMonth = ( // width code starts const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime(); - const widthDaysDifference: number = Math.abs( - Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)) - ); + const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24))); scrollWidth = (widthDaysDifference + 1) * chartData.data.width + 1; // width code ends diff --git a/web/components/gantt-chart/views/quater-view.ts b/web/components/gantt-chart/views/quater-view.ts index 0714cb28a08..ed25974a3a5 100644 --- a/web/components/gantt-chart/views/quater-view.ts +++ b/web/components/gantt-chart/views/quater-view.ts @@ -36,10 +36,7 @@ const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) return weekPayload; }; -export const generateQuarterChart = ( - quarterPayload: ChartDataType, - side: null | "left" | "right" -) => { +export const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left" | "right") => { let renderState = quarterPayload; const renderPayload: any = []; diff --git a/web/components/gantt-chart/views/week-view.ts b/web/components/gantt-chart/views/week-view.ts index 024b8d4e1b9..a65eb70b95c 100644 --- a/web/components/gantt-chart/views/week-view.ts +++ b/web/components/gantt-chart/views/week-view.ts @@ -3,12 +3,7 @@ import { ChartDataType } from "../types"; // data import { weeks, months } from "../data"; // helpers -import { - generateDate, - getWeekNumberByDate, - getNumberOfDaysInMonth, - getDatesBetweenTwoDates, -} from "./helpers"; +import { generateDate, getWeekNumberByDate, getNumberOfDaysInMonth, getDatesBetweenTwoDates } from "./helpers"; type GetAllDaysInMonthInMonthViewType = { date: any; @@ -34,9 +29,7 @@ const getAllDaysInMonthInMonthView = (month: number, year: number) => { title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, active: false, today: - currentDate.getFullYear() === year && - currentDate.getMonth() === month && - currentDate.getDate() === _day + 1 + currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === _day + 1 ? true : false, }); @@ -72,16 +65,8 @@ export const generateWeekChart = (monthPayload: ChartDataType, side: null | "lef if (side === null) { const currentDate = renderState.data.currentDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -96,16 +81,8 @@ export const generateWeekChart = (monthPayload: ChartDataType, side: null | "lef } else if (side === "left") { const currentDate = renderState.data.startDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - range, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() - 1, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); @@ -116,16 +93,8 @@ export const generateWeekChart = (monthPayload: ChartDataType, side: null | "lef } else if (side === "right") { const currentDate = renderState.data.endDate; - minusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + 1, - currentDate.getDate() - ); - plusDate = new Date( - currentDate.getFullYear(), - currentDate.getMonth() + range, - currentDate.getDate() - ); + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate()); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 4106f443b2e..163775599b3 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -31,6 +31,9 @@ export const CycleIssuesHeader: React.FC = observer(() => { cycle: cycleStore, cycleIssueFilter: cycleIssueFilterStore, project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, + commandPalette: commandPaletteStore, } = useMobxStore(); const { currentProjectDetails } = projectStore; @@ -100,7 +103,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { [issueFilterStore, projectId, workspaceSlug] ); - const cyclesList = projectId ? cycleStore.cycles[projectId.toString()] : undefined; + const cyclesList = cycleStore.projectCycles; const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; return ( @@ -139,7 +142,6 @@ export const CycleIssuesHeader: React.FC = observer(() => { type="component" component={ @@ -148,6 +150,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { } className="ml-1.5 flex-shrink-0" width="auto" + placement="bottom-start" > {cyclesList?.map((cycle) => ( { activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> @@ -194,16 +197,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { -
); -}; +}); diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index 7c41ba327d2..5be96690635 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -41,6 +41,7 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { globalViewFilters: globalViewFiltersStore, workspaceFilter: workspaceFilterStore, workspace: workspaceStore, + workspaceMember: { workspaceMembers }, project: projectStore, } = useMobxStore(); @@ -145,7 +146,7 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { handleFiltersUpdate={handleFiltersUpdate} layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet} labels={workspaceStore.workspaceLabels ?? undefined} - members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined} + members={workspaceMembers?.map((m) => m.member) ?? undefined} projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 42c01d5319b..8e1c20da37b 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -31,6 +31,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => { module: moduleStore, moduleFilter: moduleFilterStore, project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, + commandPalette: commandPaletteStore, } = useMobxStore(); const activeLayout = issueFilterStore.userDisplayFilters.layout; const { currentProjectDetails } = projectStore; @@ -146,6 +149,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { } className="ml-1.5 flex-shrink-0" width="auto" + placement="bottom-start" > {modulesList?.map((module) => ( { activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> @@ -192,16 +196,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { - diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index 8b9fa433d80..a0bf29d0526 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -24,10 +24,11 @@ const pageService = new PageService(); export const PageDetailsHeader: FC = observer((props) => { const { showButton = false } = props; + const router = useRouter(); const { workspaceSlug, pageId } = router.query; - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { currentProjectDetails } = projectStore; const { data: pageDetails } = useSWR( @@ -78,10 +79,7 @@ export const PageDetailsHeader: FC = observer((props) => { variant="primary" prependIcon={} size="sm" - onClick={() => { - const e = new KeyboardEvent("keydown", { key: "d" }); - document.dispatchEvent(e); - }} + onClick={() => commandPaletteStore.toggleCreatePageModal(true)} > Create Page diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index 634dd0c385f..0a3fd53f61e 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -18,7 +18,7 @@ export const PagesHeader: FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { currentProjectDetails } = projectStore; return ( @@ -56,10 +56,7 @@ export const PagesHeader: FC = observer((props) => { variant="primary" prependIcon={} size="sm" - onClick={() => { - const e = new KeyboardEvent("keydown", { key: "d" }); - document.dispatchEvent(e); - }} + onClick={() => commandPaletteStore.toggleCreatePageModal(true)} > Create Page diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index e68f4ce8411..3fa1b65b00a 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -5,10 +5,8 @@ import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -// helper -import { truncateText } from "helpers/string.helper"; // ui -import { Breadcrumbs, BreadcrumbItem, LayersIcon } from "@plane/ui"; +import { Breadcrumbs, LayersIcon } from "@plane/ui"; // icons import { ArrowLeft } from "lucide-react"; // components @@ -22,7 +20,12 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { project: projectStore, archivedIssueFilters: archivedIssueFiltersStore } = useMobxStore(); + const { + project: projectStore, + projectMember: { projectMembers }, + archivedIssueFilters: archivedIssueFiltersStore, + projectState: projectStateStore, + } = useMobxStore(); const { currentProjectDetails } = projectStore; @@ -117,8 +120,8 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.archived_issues[activeLayout] : undefined } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index f9bf6ec5897..ebc6cc5850b 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -23,7 +23,14 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueFilter: issueFilterStore, project: projectStore, inbox: inboxStore } = useMobxStore(); + const { + issueFilter: issueFilterStore, + project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, + inbox: inboxStore, + commandPalette: commandPaletteStore, + } = useMobxStore(); const activeLayout = issueFilterStore.userDisplayFilters.layout; @@ -166,8 +173,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => { activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> @@ -198,16 +205,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { -
diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 50d97505c95..55eeac5c271 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -23,6 +23,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { issueFilter: issueFilterStore, projectViewFilters: projectViewFiltersStore, project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, projectViews: projectViewsStore, } = useMobxStore(); @@ -162,8 +164,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined } labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index 3c56f239ea3..fa51493ed61 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -11,7 +11,7 @@ export const ProjectsHeader = observer(() => { const { workspaceSlug } = router.query; // store - const { project: projectStore } = useMobxStore(); + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : []; @@ -43,14 +43,7 @@ export const ProjectsHeader = observer(() => {
)} -
diff --git a/web/components/icons/completed-cycle-icon.tsx b/web/components/icons/completed-cycle-icon.tsx index 77d30b24ba2..d16009ad72f 100644 --- a/web/components/icons/completed-cycle-icon.tsx +++ b/web/components/icons/completed-cycle-icon.tsx @@ -2,12 +2,7 @@ import React from "react"; import type { Props } from "./types"; -export const CompletedCycleIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const CompletedCycleIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const CurrentCycleIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "24", - height = "24", - className, - color, -}) => ( +export const DocumentIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const ExternalLinkIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "20", - height = "20", - className, -}) => ( +export const ModuleCancelledIcon: React.FC = ({ width = "20", height = "20", className }) => ( = ({ - width = "20", - height = "20", - className, -}) => ( +export const ModuleCompletedIcon: React.FC = ({ width = "20", height = "20", className }) => ( = ({ - width = "20", - height = "20", - className, -}) => ( +export const ModuleInProgressIcon: React.FC = ({ width = "20", height = "20", className }) => ( = ({ - + = ({ - status, - className, - height = "12px", - width = "12px", -}) => { - if (status === "backlog") - return ; - else if (status === "cancelled") - return ; - else if (status === "completed") - return ; +export const ModuleStatusIcon: React.FC = ({ status, className, height = "12px", width = "12px" }) => { + if (status === "backlog") return ; + else if (status === "cancelled") return ; + else if (status === "completed") return ; else if (status === "in-progress") return ; - else if (status === "paused") - return ; + else if (status === "paused") return ; else return ; }; diff --git a/web/components/icons/pencil-scribble-icon.tsx b/web/components/icons/pencil-scribble-icon.tsx index 561a5bcc3f2..4d7489049b9 100644 --- a/web/components/icons/pencil-scribble-icon.tsx +++ b/web/components/icons/pencil-scribble-icon.tsx @@ -2,19 +2,8 @@ import React from "react"; import type { Props } from "./types"; -export const PencilScribbleIcon: React.FC = ({ - width = "20", - height = "20", - className, - color = "#000000", -}) => ( - +export const PencilScribbleIcon: React.FC = ({ width = "20", height = "20", className, color = "#000000" }) => ( + = ({ - width = "24", - height = "24", - className, -}) => ( +export const QuestionMarkCircleIcon: React.FC = ({ width = "24", height = "24", className }) => ( = ({ - width = "24", - height = "24", - className, - color, -}) => ( +export const SingleCommentCard: React.FC = ({ width = "24", height = "24", className, color }) => ( = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const TagIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "24", - height = "24", - className, -}) => ( +export const TriangleExclamationIcon: React.FC = ({ width = "24", height = "24", className }) => ( = ({ - width = "24", - height = "24", - className, - color = "black", -}) => ( +export const UpcomingCycleIcon: React.FC = ({ width = "24", height = "24", className, color = "black" }) => ( = ({ - width = "24", - height = "24", - className, - color, -}) => ( +export const UserGroupIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( = observer((props) => { const editorRef = useRef(null); + const editorSuggestion = useEditorSuggestions(); + const router = useRouter(); const { workspaceSlug, projectId, inboxId } = router.query; @@ -134,6 +137,7 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { control={control} render={({ field: { value, onChange } }) => ( = observer((props) => { onChange={(description, description_html: string) => { onChange(description_html); }} + mentionSuggestions={editorSuggestion.mentionSuggestions} + mentionHighlights={editorSuggestion.mentionHighlights} /> )} /> diff --git a/web/components/integration/github/auth.tsx b/web/components/integration/github/auth.tsx index c94bfacd572..9d5816f3b09 100644 --- a/web/components/integration/github/auth.tsx +++ b/web/components/integration/github/auth.tsx @@ -4,14 +4,24 @@ import useIntegrationPopup from "hooks/use-integration-popup"; import { Button } from "@plane/ui"; // types import { IWorkspaceIntegration } from "types"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { workspaceIntegration: false | IWorkspaceIntegration | undefined; provider: string | undefined; }; -export const GithubAuth: React.FC = ({ workspaceIntegration, provider }) => { - const { startAuth, isConnecting } = useIntegrationPopup(provider); +export const GithubAuth: React.FC = observer(({ workspaceIntegration, provider }) => { + const { + appConfig: { envConfig }, + } = useMobxStore(); + // hooks + const { startAuth, isConnecting } = useIntegrationPopup({ + provider, + github_app_name: envConfig?.github_app_name || "", + slack_client_id: envConfig?.slack_client_id || "", + }); return (
@@ -26,4 +36,4 @@ export const GithubAuth: React.FC = ({ workspaceIntegration, provider }) )}
); -}; +}); diff --git a/web/components/integration/guide.tsx b/web/components/integration/guide.tsx index a0876e673c7..e521ca8f59e 100644 --- a/web/components/integration/guide.tsx +++ b/web/components/integration/guide.tsx @@ -1,11 +1,8 @@ import { useState } from "react"; - import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; - import useSWR, { mutate } from "swr"; - // hooks import useUserAuth from "hooks/use-user-auth"; // services @@ -21,7 +18,7 @@ import { IImporterService } from "types"; // fetch-keys import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; // constants -import { IMPORTERS_EXPORTERS_LIST } from "constants/workspace"; +import { IMPORTERS_LIST } from "constants/workspace"; // services const integrationService = new IntegrationService(); @@ -76,10 +73,10 @@ const IntegrationGuide = () => {
*/} - {IMPORTERS_EXPORTERS_LIST.map((service) => ( + {IMPORTERS_LIST.map((service) => (
@@ -100,7 +97,7 @@ const IntegrationGuide = () => {
))}
-
+

Previous Imports

-

- {watch("data.users").filter((user) => user.import).length} -

+

{watch("data.users").filter((user) => user.import).length}

User

diff --git a/web/components/integration/jira/give-details.tsx b/web/components/integration/jira/give-details.tsx index 622517439aa..8a7c841deb4 100644 --- a/web/components/integration/jira/give-details.tsx +++ b/web/components/integration/jira/give-details.tsx @@ -16,15 +16,14 @@ export const JiraGetImportDetail: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; + const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); + const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; + const { control, formState: { errors }, } = useFormContext(); - const { project: projectStore } = useMobxStore(); - - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; - return (
@@ -190,10 +189,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
+ } /> )} /> )} /> - -
diff --git a/web/components/issues/comment/comment-card.tsx b/web/components/issues/comment/comment-card.tsx index e409122e929..983bfbb711a 100644 --- a/web/components/issues/comment/comment-card.tsx +++ b/web/components/issues/comment/comment-card.tsx @@ -40,7 +40,7 @@ export const CommentCard: React.FC = ({ const editorRef = React.useRef(null); const showEditorRef = React.useRef(null); - const editorSuggestions = useEditorSuggestions(workspaceSlug, comment.project_detail.id) + const editorSuggestions = useEditorSuggestions(); const [isEditing, setIsEditing] = useState(false); @@ -105,6 +105,7 @@ export const CommentCard: React.FC = ({
= (props) => { const { setShowAlert } = useReloadConfirmations(); - const editorSuggestion = useEditorSuggestions(workspaceSlug, issue.project_id) + const editorSuggestion = useEditorSuggestions(); const { handleSubmit, @@ -145,6 +145,7 @@ export const IssueDescriptionForm: FC = (props) => { control={control} render={({ field: { value, onChange } }) => ( = (props) => { )} />
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index b923bae6340..b006bfabcee 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -122,7 +122,7 @@ export const DraftIssueForm: FC = (props) => { const { setToastAlert } = useToast(); - const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId) + const editorSuggestions = useEditorSuggestions(); const { formState: { errors, isSubmitting }, @@ -425,6 +425,7 @@ export const DraftIssueForm: FC = (props) => { control={control} render={({ field: { value, onChange } }) => ( = observer((props) => { const user = userStore.currentUser; - const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, projectId); + const editorSuggestion = useEditorSuggestions(); const { setToastAlert } = useToast(); @@ -272,9 +272,13 @@ export const IssueForm: FC = observer((props) => { ( + rules={{ + required: true, + }} + render={({ field: { value, onChange }, fieldState: { error } }) => ( { onChange(val); setActiveProject(val); @@ -335,8 +339,8 @@ export const IssueForm: FC = observer((props) => { onChange={onChange} ref={ref} hasError={Boolean(errors.name)} - placeholder="Title" - className="resize-none text-xl w-full" + placeholder="Issue Title" + className="resize-none text-xl w-full focus:border-blue-400" /> )} /> @@ -344,7 +348,7 @@ export const IssueForm: FC = observer((props) => { )} {(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
-
+
{issueName && issueName !== "" && ( } + placement="bottom-start" > {watch("parent") ? ( <> @@ -599,24 +603,27 @@ export const IssueForm: FC = observer((props) => {
-
+
setCreateMore((prevData) => !prevData)} > +
+ {}} size="sm" /> +
Create more - {}} size="md" />
-
)} diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 642e1dad9b4..7d4a5104f61 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,38 +1,80 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useToast from "hooks/use-toast"; // components import { EmptyState } from "components/common"; +import { ExistingIssuesListModal } from "components/core"; +// ui +import { Button } from "@plane/ui"; // assets import emptyIssue from "public/empty-state/issue.svg"; -import { Button } from "@plane/ui"; +// types +import { ISearchIssueResponse } from "types"; type Props = { - openIssuesListModal: () => void; + workspaceSlug: string | undefined; + projectId: string | undefined; + cycleId: string | undefined; }; -export const CycleEmptyState: React.FC = ({ openIssuesListModal }) => ( -
- , - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }, - }} - secondaryButton={ - - } - /> -
-); +export const CycleEmptyState: React.FC = observer((props) => { + const { workspaceSlug, projectId, cycleId } = props; + // states + const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); + + const { cycleIssue: cycleIssueStore, commandPalette: commandPaletteStore } = useMobxStore(); + + const { setToastAlert } = useToast(); + + const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !projectId || !cycleId) return; + + const issueIds = data.map((i) => i.id); + + await cycleIssueStore + .addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Selected issues could not be added to the cycle. Please try again.", + }); + }); + }; + + return ( + <> + setCycleIssuesListModal(false)} + searchParams={{ cycle: true }} + handleOnSubmit={handleAddIssuesToCycle} + /> +
+ , + onClick: () => commandPaletteStore.toggleCreateIssueModal(true), + }} + secondaryButton={ + + } + /> +
+ + ); +}); 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 dc782912725..bbe5f16a527 100644 --- a/web/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/global-view.tsx @@ -1,25 +1,49 @@ -import { PlusIcon } from "lucide-react"; +// next +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; +import emptyProject from "public/empty-state/project.svg"; +// icons +import { Plus, PlusIcon } from "lucide-react"; -export const GlobalViewEmptyState: React.FC = () => ( -
- , - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }, - }} - /> -
-); +export const GlobalViewEmptyState: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { commandPalette: commandPaletteStore, project: projectStore } = useMobxStore(); + + const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; + + return ( +
+ {!projects || projects?.length === 0 ? ( + , + text: "New Project", + onClick: () => commandPaletteStore.toggleCreateProjectModal(true), + }} + /> + ) : ( + , + onClick: () => commandPaletteStore.toggleCreateIssueModal(true), + }} + /> + )} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index a71be523f92..26e34dd2848 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -4,36 +4,73 @@ import { EmptyState } from "components/common"; import { Button } from "@plane/ui"; // assets import emptyIssue from "public/empty-state/issue.svg"; +import { ExistingIssuesListModal } from "components/core"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +import { ISearchIssueResponse } from "types"; +import useToast from "hooks/use-toast"; +import { useState } from "react"; type Props = { - openIssuesListModal: () => void; + workspaceSlug: string | undefined; + projectId: string | undefined; + moduleId: string | undefined; }; -export const ModuleEmptyState: React.FC = ({ openIssuesListModal }) => ( -
- , - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }, - }} - secondaryButton={ - - } - /> -
-); +export const ModuleEmptyState: React.FC = observer((props) => { + const { workspaceSlug, projectId, moduleId } = props; + // states + const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); + + const { moduleIssue: moduleIssueStore, commandPalette: commandPaletteStore } = useMobxStore(); + + const { setToastAlert } = useToast(); + + const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { + if (!workspaceSlug || !projectId || !moduleId) return; + + const issueIds = data.map((i) => i.id); + + await moduleIssueStore + .addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Selected issues could not be added to the module. Please try again.", + }) + ); + }; + + return ( + <> + setModuleIssuesListModal(false)} + searchParams={{ module: true }} + handleOnSubmit={handleAddIssuesToModule} + /> +
+ , + onClick: () => commandPaletteStore.toggleCreateIssueModal(true), + }} + secondaryButton={ + + } + /> +
+ + ); +}); 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 2b046a14f4e..eb019f8594e 100644 --- a/web/components/issues/issue-layouts/empty-states/project-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-view.tsx @@ -1,25 +1,27 @@ +import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; -export const ProjectViewEmptyState: React.FC = () => ( -
- , - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }, - }} - /> -
-); +export const ProjectViewEmptyState: React.FC = observer(() => { + const { commandPalette: commandPaletteStore } = useMobxStore(); + + return ( +
+ , + onClick: () => commandPaletteStore.toggleCreateIssueModal(true), + }} + /> +
+ ); +}); diff --git a/web/components/issues/issue-layouts/empty-states/project.tsx b/web/components/issues/issue-layouts/empty-states/project.tsx index 03c4522c0db..3d9b6c3d40c 100644 --- a/web/components/issues/issue-layouts/empty-states/project.tsx +++ b/web/components/issues/issue-layouts/empty-states/project.tsx @@ -1,25 +1,27 @@ +import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; -export const ProjectEmptyState: React.FC = () => ( -
- , - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }, - }} - /> -
-); +export const ProjectEmptyState: React.FC = observer(() => { + const { commandPalette: commandPaletteStore } = useMobxStore(); + + return ( +
+ , + onClick: () => commandPaletteStore.toggleCreateIssueModal(true), + }} + /> +
+ ); +}); 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 a94be20a632..cf352583861 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/date.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/date.tsx @@ -36,7 +36,7 @@ export const AppliedDateFilters: React.FC = observer((props) => { }; return ( -
+ <> {values.map((date) => (
{getDateLabel(date)} @@ -49,6 +49,6 @@ export const AppliedDateFilters: React.FC = observer((props) => {
))} -
+ ); }); 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 83c072ede28..f7672ac7509 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 @@ -15,7 +15,7 @@ import { X } from "lucide-react"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // types -import { IIssueFilterOptions, IIssueLabels, IProject, IStateResponse, IUserLite } from "types"; +import { IIssueFilterOptions, IIssueLabels, IProject, IState, IUserLite } from "types"; type Props = { appliedFilters: IIssueFilterOptions; @@ -24,7 +24,7 @@ type Props = { labels?: IIssueLabels[] | undefined; members?: IUserLite[] | undefined; projects?: IProject[] | undefined; - states?: IStateResponse | undefined; + states?: IState[] | undefined; }; const membersFilters = ["assignees", "mentions", "created_by", "subscriber"]; @@ -50,50 +50,55 @@ export const AppliedFiltersList: React.FC = observer((props) => { className="capitalize py-1 px-2 border border-custom-border-200 rounded-md flex items-center gap-2 flex-wrap" > {replaceUnderscoreIfSnakeCase(filterKey)} - {membersFilters.includes(filterKey) && ( - handleRemoveFilter(filterKey, val)} - members={members} - values={value} - /> - )} - {dateFilters.includes(filterKey) && ( - handleRemoveFilter(filterKey, val)} values={value} /> - )} - {filterKey === "labels" && ( - handleRemoveFilter("labels", val)} - labels={labels} - values={value} - /> - )} - {filterKey === "priority" && ( - handleRemoveFilter("priority", val)} values={value} /> - )} - {filterKey === "state" && ( - handleRemoveFilter("state", val)} - states={states} - values={value} - /> - )} - {filterKey === "state_group" && ( - handleRemoveFilter("state_group", val)} values={value} /> - )} - {filterKey === "project" && ( - handleRemoveFilter("project", val)} - projects={projects} - values={value} - /> - )} - +
+ {membersFilters.includes(filterKey) && ( + handleRemoveFilter(filterKey, val)} + members={members} + values={value} + /> + )} + {dateFilters.includes(filterKey) && ( + handleRemoveFilter(filterKey, val)} values={value} /> + )} + {filterKey === "labels" && ( + handleRemoveFilter("labels", val)} + labels={labels} + values={value} + /> + )} + {filterKey === "priority" && ( + handleRemoveFilter("priority", val)} values={value} /> + )} + {filterKey === "state" && states && ( + handleRemoveFilter("state", val)} + states={states} + values={value} + /> + )} + {filterKey === "state_group" && ( + handleRemoveFilter("state_group", val)} + values={value} + /> + )} + {filterKey === "project" && ( + handleRemoveFilter("project", val)} + projects={projects} + values={value} + /> + )} + +
); })} diff --git a/web/components/issues/issue-layouts/filters/applied-filters/label.tsx b/web/components/issues/issue-layouts/filters/applied-filters/label.tsx index abc5a8b2c9f..ee597575f5d 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/label.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/label.tsx @@ -15,7 +15,7 @@ export const AppliedLabelsFilters: React.FC = observer((props) => { const { handleRemove, labels, values } = props; return ( -
+ <> {values.map((labelId) => { const labelDetails = labels?.find((l) => l.id === labelId); @@ -40,6 +40,6 @@ export const AppliedLabelsFilters: React.FC = observer((props) => {
); })} -
+ ); }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx index cef65882cb6..dc8ac397c6c 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx @@ -15,7 +15,7 @@ export const AppliedMembersFilters: React.FC = observer((props) => { const { handleRemove, members, values } = props; return ( -
+ <> {values.map((memberId) => { const memberDetails = members?.find((m) => m.id === memberId); @@ -35,6 +35,6 @@ export const AppliedMembersFilters: React.FC = observer((props) => {
); })} -
+ ); }); 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 7b76f8b5140..198589a2673 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx @@ -15,7 +15,7 @@ export const AppliedPriorityFilters: React.FC = observer((props) => { const { handleRemove, values } = props; return ( -
+ <> {values.map((priority) => (
= observer((props) => {
))} -
+ ); }); 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 d8bc758ea96..51b49fbffe2 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -16,7 +16,7 @@ export const AppliedProjectFilters: React.FC = observer((props) => { const { handleRemove, projects, values } = props; return ( -
+ <> {values.map((projectId) => { const projectDetails = projects?.find((p) => p.id === projectId); @@ -44,6 +44,6 @@ export const AppliedProjectFilters: React.FC = observer((props) => {
); })} -
+ ); }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx index 20e20d34e37..32e5a4a2191 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx @@ -12,7 +12,12 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { archivedIssueFilters: archivedIssueFiltersStore, project: projectStore } = useMobxStore(); + const { + archivedIssueFilters: archivedIssueFiltersStore, + project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, + } = useMobxStore(); const userFilters = archivedIssueFiltersStore.userFilters; @@ -73,8 +78,8 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""]} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""]} />
); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index 5910eff960a..2bd0ffdfe1d 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -1,6 +1,5 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components @@ -12,7 +11,12 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const { project: projectStore, cycleIssueFilter: cycleIssueFilterStore } = useMobxStore(); + const { + project: projectStore, + projectMember: { projectMembers }, + cycleIssueFilter: cycleIssueFilterStore, + projectState: projectStateStore, + } = useMobxStore(); const userFilters = cycleIssueFilterStore.cycleFilters; @@ -69,8 +73,8 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""]} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""]} />
); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index ad1712b55e3..9bc94b0c9d1 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -22,6 +22,7 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => { globalViewFilters: globalViewFiltersStore, project: projectStore, workspace: workspaceStore, + workspaceMember: { workspaceMembers }, } = useMobxStore(); const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined; @@ -101,7 +102,7 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={workspaceStore.workspaceLabels ?? undefined} - members={workspaceStore.workspaceMembers?.map((m) => m.member)} + members={workspaceMembers?.map((m) => m.member)} projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} /> {storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data.filters ?? {}) && ( diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index 6bcfb40f56d..6a5cd694456 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -12,7 +12,12 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; - const { project: projectStore, moduleFilter: moduleFilterStore } = useMobxStore(); + const { + project: projectStore, + moduleFilter: moduleFilterStore, + projectState: projectStateStore, + projectMember: { projectMembers }, + } = useMobxStore(); const userFilters = moduleFilterStore.moduleFilters; @@ -69,8 +74,8 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""]} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""]} />
); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index 505649bce30..26f475733b0 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -12,7 +12,12 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore(); + const { + issueFilter: issueFilterStore, + project: projectStore, + projectState: projectStateStore, + projectMember: { projectMembers }, + } = useMobxStore(); const userFilters = issueFilterStore.userFilters; @@ -73,8 +78,8 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""]} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""]} />
); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index c8ee36fe75d..5f797a6ce5e 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -19,6 +19,8 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { const { project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, projectViews: projectViewsStore, projectViewFilters: projectViewFiltersStore, } = useMobxStore(); @@ -98,13 +100,15 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} - members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""]} + members={projectMembers?.map((m) => m.member)} + states={projectStateStore.states?.[projectId?.toString() ?? ""]} /> {storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data ?? {}) && ( - +
+ +
)}
); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx index d0707efda44..eadf34005e2 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx @@ -14,7 +14,7 @@ export const AppliedStateGroupFilters: React.FC = observer((props) => { const { handleRemove, values } = props; return ( -
+ <> {values.map((stateGroup) => (
@@ -28,6 +28,6 @@ export const AppliedStateGroupFilters: React.FC = observer((props) => {
))} -
+ ); }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx index 5293a623a3b..c307d21c559 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx @@ -3,26 +3,22 @@ import { observer } from "mobx-react-lite"; // icons import { StateGroupIcon } from "@plane/ui"; import { X } from "lucide-react"; -// helpers -import { getStatesList } from "helpers/state.helper"; // types -import { IStateResponse } from "types"; +import { IState } from "types"; type Props = { handleRemove: (val: string) => void; - states: IStateResponse | undefined; + states: IState[]; values: string[]; }; export const AppliedStateFilters: React.FC = observer((props) => { const { handleRemove, states, values } = props; - const statesList = getStatesList(states); - return ( -
+ <> {values.map((stateId) => { - const stateDetails = statesList?.find((s) => s.id === stateId); + const stateDetails = states?.find((s) => s.id === stateId); if (!stateDetails) return null; @@ -40,6 +36,6 @@ export const AppliedStateFilters: React.FC = observer((props) => {
); })} -
+ ); }); 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 f07a47974c8..781e7fcd5af 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 @@ -15,7 +15,7 @@ import { FilterTargetDate, } from "components/issues"; // types -import { IIssueFilterOptions, IIssueLabels, IProject, IStateResponse, IUserLite } from "types"; +import { IIssueFilterOptions, IIssueLabels, IProject, IState, IUserLite } from "types"; // constants import { ILayoutDisplayFiltersOptions } from "constants/issue"; @@ -26,7 +26,7 @@ type Props = { labels?: IIssueLabels[] | undefined; members?: IUserLite[] | undefined; projects?: IProject[] | undefined; - states?: IStateResponse | undefined; + states?: IState[] | undefined; }; export const FilterSelection: React.FC = observer((props) => { 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 20caebc20f9..6f193d02938 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state.tsx @@ -1,19 +1,16 @@ import React, { useState } from "react"; - // components import { FilterHeader, FilterOption } from "components/issues"; // ui import { Loader, StateGroupIcon } from "@plane/ui"; -// helpers -import { getStatesList } from "helpers/state.helper"; // types -import { IStateResponse } from "types"; +import { IState } from "types"; type Props = { appliedFilters: string[] | null; handleUpdate: (val: string) => void; searchQuery: string; - states: IStateResponse | undefined; + states: IState[] | undefined; }; export const FilterState: React.FC = (props) => { @@ -22,11 +19,9 @@ export const FilterState: React.FC = (props) => { const [itemsToRender, setItemsToRender] = useState(5); const [previewEnabled, setPreviewEnabled] = useState(true); - const statesList = getStatesList(states); - const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = statesList?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); + const filteredOptions = states?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); const handleViewToggle = () => { if (!filteredOptions) return; diff --git a/web/components/issues/issue-layouts/gantt/cycle-root.tsx b/web/components/issues/issue-layouts/gantt/cycle-root.tsx index 78ba7f2a6c5..56a17db15da 100644 --- a/web/components/issues/issue-layouts/gantt/cycle-root.tsx +++ b/web/components/issues/issue-layouts/gantt/cycle-root.tsx @@ -4,8 +4,13 @@ import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; import useProjectDetails from "hooks/use-project-details"; // components -import { GanttChartRoot, IBlockUpdateData, renderIssueBlocksStructure } from "components/gantt-chart"; -import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; +import { IssueGanttBlock } from "components/issues"; +import { + GanttChartRoot, + IBlockUpdateData, + renderIssueBlocksStructure, + IssueGanttSidebar, +} from "components/gantt-chart"; // types import { IIssueUnGroupedStructure } from "store/issue"; import { IIssue } from "types"; @@ -40,7 +45,7 @@ export const CycleGanttLayout: React.FC = observer(() => { blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null} blockUpdateHandler={updateIssue} blockToRender={(data: IIssue) => } - sidebarBlockToRender={(data: IIssue) => } + sidebarToRender={(props) => } enableBlockLeftResize={isAllowed} enableBlockRightResize={isAllowed} enableBlockMove={isAllowed} diff --git a/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx index 8ce451e2b52..0c87e695018 100644 --- a/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx @@ -14,8 +14,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { renderDateFormat } from "helpers/date-time.helper"; // types import { IIssue } from "types"; -// constants -import { createIssuePayload } from "constants/issue"; +// helpers +import { createIssuePayload } from "helpers/issue.helper"; type Props = { prePopulatedData?: Partial; diff --git a/web/components/issues/issue-layouts/gantt/module-root.tsx b/web/components/issues/issue-layouts/gantt/module-root.tsx index 7584cc7c6c8..c5a880bbea8 100644 --- a/web/components/issues/issue-layouts/gantt/module-root.tsx +++ b/web/components/issues/issue-layouts/gantt/module-root.tsx @@ -4,8 +4,13 @@ import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; import useProjectDetails from "hooks/use-project-details"; // components -import { GanttChartRoot, IBlockUpdateData, renderIssueBlocksStructure } from "components/gantt-chart"; import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; +import { + GanttChartRoot, + IBlockUpdateData, + renderIssueBlocksStructure, + IssueGanttSidebar, +} from "components/gantt-chart"; // types import { IIssueUnGroupedStructure } from "store/issue"; import { IIssue } from "types"; @@ -39,8 +44,8 @@ export const ModuleGanttLayout: React.FC = observer(() => { loaderTitle="Issues" blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null} blockUpdateHandler={updateIssue} + sidebarToRender={(data) => } blockToRender={(data: IIssue) => } - sidebarBlockToRender={(data: IIssue) => } enableBlockLeftResize={isAllowed} enableBlockRightResize={isAllowed} enableBlockMove={isAllowed} 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 1be9ad0c3d6..c06afd8170e 100644 --- a/web/components/issues/issue-layouts/gantt/project-view-root.tsx +++ b/web/components/issues/issue-layouts/gantt/project-view-root.tsx @@ -6,8 +6,13 @@ import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useProjectDetails from "hooks/use-project-details"; // components -import { GanttChartRoot, IBlockUpdateData, renderIssueBlocksStructure } from "components/gantt-chart"; -import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; +import { IssueGanttBlock } from "components/issues"; +import { + GanttChartRoot, + IBlockUpdateData, + renderIssueBlocksStructure, + ProjectViewGanttSidebar, +} from "components/gantt-chart"; // types import { IIssueUnGroupedStructure } from "store/issue"; import { IIssue } from "types"; @@ -42,7 +47,7 @@ export const ProjectViewGanttLayout: React.FC = observer(() => { blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null} blockUpdateHandler={updateIssue} blockToRender={(data: IIssue) => } - sidebarBlockToRender={(data: IIssue) => } + sidebarToRender={(props) => } enableBlockLeftResize={isAllowed} enableBlockRightResize={isAllowed} enableBlockMove={isAllowed} diff --git a/web/components/issues/issue-layouts/gantt/root.tsx b/web/components/issues/issue-layouts/gantt/root.tsx index 77bbc0710df..d7ddd69b2b8 100644 --- a/web/components/issues/issue-layouts/gantt/root.tsx +++ b/web/components/issues/issue-layouts/gantt/root.tsx @@ -5,8 +5,13 @@ import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; import useProjectDetails from "hooks/use-project-details"; // components -import { GanttChartRoot, IBlockUpdateData, renderIssueBlocksStructure } from "components/gantt-chart"; -import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; +import { IssueGanttBlock } from "components/issues"; +import { + GanttChartRoot, + IBlockUpdateData, + renderIssueBlocksStructure, + IssueGanttSidebar, +} from "components/gantt-chart"; // types import { IIssueUnGroupedStructure } from "store/issue"; import { IIssue } from "types"; @@ -41,7 +46,7 @@ export const GanttLayout: React.FC = observer(() => { blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null} blockUpdateHandler={updateIssue} blockToRender={(data: IIssue) => } - sidebarBlockToRender={(data: IIssue) => } + sidebarToRender={(props) => } enableBlockLeftResize={isAllowed} enableBlockRightResize={isAllowed} enableBlockMove={isAllowed} diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 34ed0238276..9d4c8aedd23 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -42,7 +42,7 @@ export const KanbanIssueBlock: React.FC = (props) => { return ( <> - + {(provided, snapshot) => (
= (props) => { )}
diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 1e252895b80..eee31e7c306 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -10,11 +10,13 @@ import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/is import { IIssueDisplayProperties, IIssue } from "types"; // constants import { getValueFromObject } from "constants/issue"; +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; export interface IGroupByKanBan { issues: any; sub_group_by: string | null; group_by: string | null; + order_by: string | null; sub_group_id: string; list: any; listKey: string; @@ -31,6 +33,7 @@ export interface IGroupByKanBan { kanBanToggle: any; handleKanBanToggle: any; enableQuickIssueCreate?: boolean; + isDragStarted?: boolean; } const GroupByKanBan: React.FC = observer((props) => { @@ -38,6 +41,7 @@ const GroupByKanBan: React.FC = observer((props) => { issues, sub_group_by, group_by, + order_by, sub_group_id = "null", list, listKey, @@ -49,17 +53,20 @@ const GroupByKanBan: React.FC = observer((props) => { kanBanToggle, handleKanBanToggle, enableQuickIssueCreate, + isDragStarted, } = props; const verticalAlignPosition = (_list: any) => kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string); return ( -
+
{list && list.length > 0 && list.map((_list: any) => ( -
+
{sub_group_by === null && (
= observer((props) => { verticalAlignPosition(_list) ? `w-[0px] overflow-hidden` : `w-full transition-all` }`} > - + {(provided: any, snapshot: any) => (
= observer((props) => { /> ) : ( isDragDisabled && ( -
+
{/*
Drop here
*/}
) )} + {provided.placeholder}
)}
- {enableQuickIssueCreate && ( - + +
+ {enableQuickIssueCreate && ( + + )} +
+ + {isDragStarted && isDragDisabled && ( +
+
+ {`This board is ordered by "${replaceUnderscoreIfSnakeCase( + order_by ? (order_by[0] === "-" ? order_by.slice(1) : order_by) : "created_at" + )}"`} +
+
)}
))} @@ -131,8 +155,8 @@ export interface IKanBan { issues: any; sub_group_by: string | null; group_by: string | null; + order_by: string | null; sub_group_id?: string; - handleDragDrop?: (result: any) => void | undefined; handleIssues: ( sub_group_by: string | null, group_by: string | null, @@ -151,6 +175,7 @@ export interface IKanBan { members: any; projects: any; enableQuickIssueCreate?: boolean; + isDragStarted?: boolean; } export const KanBan: React.FC = observer((props) => { @@ -158,6 +183,7 @@ export const KanBan: React.FC = observer((props) => { issues, sub_group_by, group_by, + order_by, sub_group_id = "null", handleIssues, quickActions, @@ -172,6 +198,7 @@ export const KanBan: React.FC = observer((props) => { members, projects, enableQuickIssueCreate, + isDragStarted, } = props; const { issueKanBanView: issueKanBanViewStore } = useMobxStore(); @@ -182,6 +209,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -201,6 +230,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -220,6 +251,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -239,6 +272,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -258,6 +293,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -277,6 +314,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )} @@ -296,6 +335,7 @@ export const KanBan: React.FC = observer((props) => { = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} + isDragStarted={isDragStarted} /> )}
diff --git a/web/components/issues/issue-layouts/kanban/headers/state-group.tsx b/web/components/issues/issue-layouts/kanban/headers/state-group.tsx index 33c70bdddc6..47d258c36f5 100644 --- a/web/components/issues/issue-layouts/kanban/headers/state-group.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/state-group.tsx @@ -17,7 +17,7 @@ export interface IStateGroupHeader { } export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => ( -
+
); diff --git a/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx index 750606ebbca..ad7b9362e53 100644 --- a/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx @@ -2,19 +2,16 @@ import { useEffect, useState, useRef } from "react"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { PlusIcon } from "lucide-react"; -// store import { observer } from "mobx-react-lite"; +// store import { useMobxStore } from "lib/mobx/store-provider"; - // hooks import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useProjectDetails from "hooks/use-project-details"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; - -// constants -import { createIssuePayload } from "constants/issue"; - +// helpers +import { createIssuePayload } from "helpers/issue.helper"; // types import { IIssue } from "types"; diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index 82f4920c091..a060ed213a8 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext } from "@hello-pangea/dnd"; @@ -8,8 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanSwimLanes } from "../swimlanes"; import { KanBan } from "../default"; import { CycleIssueQuickActions } from "components/issues"; -// helpers -import { orderArrayBy } from "helpers/array.helper"; +import { Spinner } from "@plane/ui"; // types import { IIssue } from "types"; // constants @@ -24,12 +23,13 @@ export const CycleKanBanLayout: React.FC = observer(() => { // store const { project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, cycleIssue: cycleIssueStore, issueFilter: issueFilterStore, cycleIssueKanBanView: cycleIssueKanBanViewStore, issueDetail: issueDetailStore, } = useMobxStore(); - const { currentProjectDetails } = projectStore; const issues = cycleIssueStore?.getIssues; @@ -37,6 +37,8 @@ export const CycleKanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; + const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null; + const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; const displayProperties = issueFilterStore?.userDisplayProperties || null; @@ -45,7 +47,15 @@ export const CycleKanBanLayout: React.FC = observer(() => { ? "swimlanes" : "default"; + const [isDragStarted, setIsDragStarted] = useState(false); + + // const onDragStart = () => { + // setIsDragStarted(true); + // }; + const onDragEnd = (result: any) => { + setIsDragStarted(false); + if (!result) return; if ( @@ -87,72 +97,83 @@ export const CycleKanBanLayout: React.FC = observer(() => { cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value); }; - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; - const estimates = - currentProjectDetails?.estimate !== null - ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null - : null; + // const estimates = + // currentProjectDetails?.estimate !== null + // ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null + // : null; return ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + <> + {cycleIssueStore.loader ? ( +
+ +
+ ) : ( +
+ + {currentKanBanView === "default" ? ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={projectMembers?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> - )} - displayProperties={displayProperties} - kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + ) : ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={projectMembers?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> )} - displayProperties={displayProperties} - kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - )} - -
+
+
+ )} + ); }); 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 be40c6bcab4..7a25f835c74 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext } from "@hello-pangea/dnd"; @@ -8,8 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanSwimLanes } from "../swimlanes"; import { KanBan } from "../default"; import { ModuleIssueQuickActions } from "components/issues"; -// helpers -import { orderArrayBy } from "helpers/array.helper"; +import { Spinner } from "@plane/ui"; // types import { IIssue } from "types"; // constants @@ -23,12 +22,13 @@ export const ModuleKanBanLayout: React.FC = observer(() => { // store const { project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, moduleIssue: moduleIssueStore, issueFilter: issueFilterStore, moduleIssueKanBanView: moduleIssueKanBanViewStore, issueDetail: issueDetailStore, } = useMobxStore(); - const { currentProjectDetails } = projectStore; const issues = moduleIssueStore?.getIssues; @@ -36,6 +36,8 @@ export const ModuleKanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; + const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null; + const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; const displayProperties = issueFilterStore?.userDisplayProperties || null; @@ -44,7 +46,14 @@ export const ModuleKanBanLayout: React.FC = observer(() => { ? "swimlanes" : "default"; + const [isDragStarted, setIsDragStarted] = useState(false); + + // const onDragStart = () => { + // setIsDragStarted(true); + // }; + const onDragEnd = (result: any) => { + setIsDragStarted(false); if (!result) return; if ( @@ -86,72 +95,83 @@ export const ModuleKanBanLayout: React.FC = observer(() => { moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value); }; - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; - const estimates = - currentProjectDetails?.estimate !== null - ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null - : null; + // const estimates = + // currentProjectDetails?.estimate !== null + // ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null + // : null; return ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + <> + {moduleIssueStore.loader ? ( +
+ +
+ ) : ( +
+ + {currentKanBanView === "default" ? ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={projectMembers?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> - )} - displayProperties={displayProperties} - kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + ) : ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={projectMembers?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> )} - displayProperties={displayProperties} - kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - )} - -
+
+
+ )} + ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index 2849315b4dd..4682f8e2635 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback } from "react"; +import { FC, useCallback, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { DragDropContext } from "@hello-pangea/dnd"; @@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanSwimLanes } from "../swimlanes"; import { KanBan } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; +import { Spinner } from "@plane/ui"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; // types @@ -19,6 +20,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { const { workspace: workspaceStore, project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, profileIssues: profileIssuesStore, profileIssueFilters: profileIssueFiltersStore, issueKanBanView: issueKanBanViewStore, @@ -34,6 +37,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; + const order_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.order_by || null; + const userDisplayFilters = profileIssueFiltersStore?.userDisplayFilters || null; const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null; @@ -42,7 +47,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { ? "swimlanes" : "default"; + const [isDragStarted, setIsDragStarted] = useState(false); + + // const onDragStart = () => { + // setIsDragStarted(true); + // }; + const onDragEnd = (result: any) => { + setIsDragStarted(false); if (!result) return; if ( @@ -75,66 +87,77 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { issueKanBanViewStore.handleKanBanToggle(toggle, value); }; - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = workspaceStore.workspaceLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = projectStore?.workspaceProjects || null; return ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + <> + {profileIssuesStore.loader ? ( +
+ +
+ ) : ( +
+ + {currentKanBanView === "default" ? ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={issueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={projectMembers?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> - )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + ) : ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={issueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={projectMembers?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - )} - -
+
+
+ )} + ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx index 179d628b4b0..8f34dee7192 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { useRouter } from "next/router"; import { DragDropContext } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; @@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanSwimLanes } from "../swimlanes"; import { KanBan } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; +import { Spinner } from "@plane/ui"; // types import { IIssue } from "types"; // constants @@ -21,6 +22,8 @@ export const KanBanLayout: React.FC = observer(() => { const { project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, issue: issueStore, issueFilter: issueFilterStore, issueKanBanView: issueKanBanViewStore, @@ -34,6 +37,8 @@ export const KanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; + const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null; + const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; const displayProperties = issueFilterStore?.userDisplayProperties || null; @@ -42,12 +47,22 @@ export const KanBanLayout: React.FC = observer(() => { ? "swimlanes" : "default"; + const [isDragStarted, setIsDragStarted] = useState(false); + + const onDragStart = () => { + setIsDragStarted(true); + }; + const onDragEnd = (result: any) => { + setIsDragStarted(false); + if (!result) return; if ( result.destination && result.source && + result.source.droppableId && + result.destination.droppableId && result.destination.droppableId === result.source.droppableId && result.destination.index === result.source.index ) @@ -75,10 +90,9 @@ export const KanBanLayout: React.FC = observer(() => { issueKanBanViewStore.handleKanBanToggle(toggle, value); }; - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = workspaceSlug ? projectStore?.projects?.[workspaceSlug] || null : null; const estimates = @@ -87,59 +101,71 @@ export const KanBanLayout: React.FC = observer(() => { : null; return ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + <> + {issueStore.loader ? ( +
+ +
+ ) : ( +
+ + {currentKanBanView === "default" ? ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={issueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={projectMembers?.map((m) => m.member) ?? null} + projects={projects} + enableQuickIssueCreate + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> - )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - enableQuickIssueCreate - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + ) : ( + ( + handleIssues(sub_group_by, group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + kanBanToggle={issueKanBanViewStore?.kanBanToggle} + handleKanBanToggle={handleKanBanToggle} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={projectMembers?.map((m) => m.member) ?? null} + projects={projects} + showEmptyGroup={userDisplayFilters?.show_empty_groups || true} + isDragStarted={isDragStarted} /> )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - /> - )} - -
+
+
+ )} + ); }); 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 583835ba345..6751f31453e 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -1,7 +1,5 @@ import React from "react"; -// react beautiful dnd import { DragDropContext } from "@hello-pangea/dnd"; -// mobx import { observer } from "mobx-react-lite"; // components import { KanBanSwimLanes } from "../swimlanes"; @@ -17,6 +15,8 @@ export interface IViewKanBanLayout {} export const ProjectViewKanBanLayout: React.FC = observer(() => { const { project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, issue: issueStore, issueFilter: issueFilterStore, issueKanBanView: issueKanBanViewStore, @@ -54,12 +54,11 @@ export const ProjectViewKanBanLayout: React.FC = observer(() => { issueStore.updateIssueStructure(group_by, sub_group_by, issue); }; - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStore?.projectStates || null; + const projects = projectStateStore?.projectStates || null; const estimates = null; return null; diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 662a9723475..e25ddea3d66 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -58,6 +58,7 @@ const SubGroupSwimlaneHeader: React.FC = ({ }; interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { + order_by: string | null; showEmptyGroup: boolean; states: IState[] | null; stateGroups: any; @@ -76,12 +77,14 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { displayProperties: IIssueDisplayProperties; kanBanToggle: any; handleKanBanToggle: any; + isDragStarted?: boolean; } const SubGroupSwimlane: React.FC = observer((props) => { const { issues, sub_group_by, group_by, + order_by, list, listKey, handleIssues, @@ -96,6 +99,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { labels, members, projects, + isDragStarted, } = props; const calculateIssueCount = (column_id: string) => { @@ -133,6 +137,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { issues={issues?.[getValueFromObject(_list, listKey) as string]} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} sub_group_id={getValueFromObject(_list, listKey) as string} handleIssues={handleIssues} quickActions={quickActions} @@ -147,6 +152,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { members={members} projects={projects} enableQuickIssueCreate + isDragStarted={isDragStarted} />
)} @@ -160,6 +166,7 @@ export interface IKanBanSwimLanes { issues: any; sub_group_by: string | null; group_by: string | null; + order_by: string | null; handleIssues: ( sub_group_by: string | null, group_by: string | null, @@ -177,6 +184,7 @@ export interface IKanBanSwimLanes { labels: IIssueLabels[] | null; members: IUserLite[] | null; projects: IProject[] | null; + isDragStarted?: boolean; } export const KanBanSwimLanes: React.FC = observer((props) => { @@ -184,6 +192,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues, sub_group_by, group_by, + order_by, handleIssues, quickActions, displayProperties, @@ -196,6 +205,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels, members, projects, + isDragStarted, } = props; return ( @@ -291,6 +301,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={projects} listKey={`id`} handleIssues={handleIssues} @@ -305,6 +316,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -313,6 +325,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={states} listKey={`id`} handleIssues={handleIssues} @@ -327,6 +340,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -335,6 +349,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={states} listKey={`id`} handleIssues={handleIssues} @@ -349,6 +364,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -357,6 +373,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={stateGroups} listKey={`key`} handleIssues={handleIssues} @@ -371,6 +388,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -379,6 +397,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={priorities} listKey={`key`} handleIssues={handleIssues} @@ -393,6 +412,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -401,6 +421,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={labels ? [...labels, { id: "None", name: "None" }] : labels} listKey={`id`} handleIssues={handleIssues} @@ -415,6 +436,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -423,6 +445,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={members ? [...members, { id: "None", display_name: "None" }] : members} listKey={`id`} handleIssues={handleIssues} @@ -437,6 +460,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )} @@ -445,6 +469,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { issues={issues} sub_group_by={sub_group_by} group_by={group_by} + order_by={order_by} list={members} listKey={`id`} handleIssues={handleIssues} @@ -459,6 +484,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { labels={labels} members={members} projects={projects} + isDragStarted={isDragStarted} /> )}
diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 52410756aeb..28f5f765a6a 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -4,20 +4,20 @@ import { IssuePeekOverview } from "components/issues/issue-peek-overview"; // ui import { Tooltip } from "@plane/ui"; // types -import { IIssue } from "types"; +import { IIssue, IIssueDisplayProperties } from "types"; interface IssueBlockProps { columnId: string; issue: IIssue; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; isReadonly?: boolean; showEmptyGroup?: boolean; } export const IssueBlock: React.FC = (props) => { - const { columnId, issue, handleIssues, quickActions, display_properties, showEmptyGroup, isReadonly } = props; + const { columnId, issue, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props; const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => { handleIssues(group_by, issueToUpdate, "update"); @@ -26,7 +26,7 @@ export const IssueBlock: React.FC = (props) => { return ( <>
- {display_properties && display_properties?.key && ( + {displayProperties && displayProperties?.key && (
{issue?.project_detail?.identifier}-{issue.sequence_id}
@@ -54,7 +54,7 @@ export const IssueBlock: React.FC = (props) => { issue={issue} isReadonly={isReadonly} handleIssues={updateIssue} - display_properties={display_properties} + displayProperties={displayProperties} showEmptyGroup={showEmptyGroup} /> {quickActions(!columnId && columnId === "null" ? null : columnId, issue)} diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 00779cc3479..22a92a15991 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; // components import { IssueBlock } from "components/issues"; // types -import { IIssue } from "types"; +import { IIssue, IIssueDisplayProperties } from "types"; interface Props { columnId: string; @@ -10,12 +10,12 @@ interface Props { isReadonly?: boolean; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; showEmptyGroup?: boolean; } export const IssueBlocksList: FC = (props) => { - const { columnId, issues, handleIssues, quickActions, display_properties, showEmptyGroup, isReadonly } = props; + const { columnId, issues, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props; return (
@@ -28,7 +28,7 @@ export const IssueBlocksList: FC = (props) => { handleIssues={handleIssues} quickActions={quickActions} isReadonly={isReadonly} - display_properties={display_properties} + displayProperties={displayProperties} showEmptyGroup={showEmptyGroup} /> )) diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 57cdeb34bdd..b9d13a92bce 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; import { ListGroupByHeaderRoot } from "./headers/group-by-root"; import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues"; // types -import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types"; +import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types"; // constants import { getValueFromObject } from "constants/issue"; @@ -16,7 +16,7 @@ export interface IGroupByList { listKey: string; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; is_list?: boolean; enableQuickIssueCreate?: boolean; showEmptyGroup?: boolean; @@ -31,7 +31,7 @@ const GroupByList: React.FC = observer((props) => { listKey, handleIssues, quickActions, - display_properties, + displayProperties, is_list = false, enableQuickIssueCreate, showEmptyGroup, @@ -59,7 +59,7 @@ const GroupByList: React.FC = observer((props) => { issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} /> @@ -86,7 +86,7 @@ export interface IList { handleDragDrop?: (result: any) => void | undefined; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; states: IState[] | null; labels: IIssueLabels[] | null; members: IUserLite[] | null; @@ -105,7 +105,7 @@ export const List: React.FC = observer((props) => { isReadonly, handleIssues, quickActions, - display_properties, + displayProperties, states, labels, members, @@ -126,7 +126,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} is_list enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} @@ -142,7 +142,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -157,7 +157,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -172,7 +172,7 @@ export const List: React.FC = observer((props) => { listKey={`key`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -187,7 +187,7 @@ export const List: React.FC = observer((props) => { listKey={`key`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -202,7 +202,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -217,7 +217,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} @@ -232,7 +232,7 @@ export const List: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} enableQuickIssueCreate={enableQuickIssueCreate} isReadonly={isReadonly} showEmptyGroup={showEmptyGroup} diff --git a/web/components/issues/issue-layouts/list/headers/state-group.tsx b/web/components/issues/issue-layouts/list/headers/state-group.tsx index 9f6610e5dc8..c6da0254396 100644 --- a/web/components/issues/issue-layouts/list/headers/state-group.tsx +++ b/web/components/issues/issue-layouts/list/headers/state-group.tsx @@ -4,6 +4,8 @@ import { observer } from "mobx-react-lite"; import { HeaderGroupByCard } from "./group-by-card"; // ui import { StateGroupIcon } from "@plane/ui"; +// helpers +import { capitalizeFirstLetter } from "helpers/string.helper"; export interface IStateGroupHeader { column_id: string; @@ -27,7 +29,7 @@ export const StateGroupHeader: FC = observer((props) => { {stateGroup && ( } - title={stateGroup?.key || ""} + title={capitalizeFirstLetter(stateGroup?.key) || ""} count={issues_count} issuePayload={{}} /> diff --git a/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx index effc96acbe3..813e25d0aec 100644 --- a/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx @@ -1,23 +1,19 @@ import { useEffect, useState, useRef } from "react"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; - +import { observer } from "mobx-react-lite"; +import { PlusIcon } from "lucide-react"; // hooks import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useProjectDetails from "hooks/use-project-details"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; - // store -import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; - -// constants -import { createIssuePayload } from "constants/issue"; - +// helpers +import { createIssuePayload } from "helpers/issue.helper"; // types import { IIssue } from "types"; -import { PlusIcon } from "lucide-react"; type Props = { groupId?: string; diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index c304867ac4b..58944c76c7e 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -11,19 +11,19 @@ import { IssuePropertyDate } from "../properties/date"; // ui import { Tooltip } from "@plane/ui"; // types -import { IIssue, IState, TIssuePriorities } from "types"; +import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types"; export interface IKanBanProperties { columnId: string; issue: IIssue; handleIssues: (group_by: string | null, issue: IIssue) => void; - display_properties: any; + displayProperties: IIssueDisplayProperties; isReadonly?: boolean; showEmptyGroup?: boolean; } export const KanBanProperties: FC = observer((props) => { - const { columnId: group_id, issue, handleIssues, display_properties, isReadonly, showEmptyGroup } = props; + const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly, showEmptyGroup } = props; const handleState = (state: IState) => { handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id }); @@ -54,10 +54,10 @@ export const KanBanProperties: FC = observer((props) => { }; return ( -
+
{/* basic properties */} {/* state */} - {display_properties && display_properties?.state && ( + {displayProperties && displayProperties?.state && ( = observer((props) => { )} {/* priority */} - {display_properties && display_properties?.priority && ( + {displayProperties && displayProperties?.priority && ( = observer((props) => { )} {/* label */} - {display_properties && display_properties?.labels && (showEmptyGroup || issue?.labels.length > 0) && ( + {displayProperties && displayProperties?.labels && (showEmptyGroup || issue?.labels.length > 0) && ( = observer((props) => { )} {/* assignee */} - {display_properties && display_properties?.assignee && (showEmptyGroup || issue?.assignees?.length > 0) && ( + {displayProperties && displayProperties?.assignee && (showEmptyGroup || issue?.assignees?.length > 0) && ( = observer((props) => { )} {/* start date */} - {display_properties && display_properties?.start_date && (showEmptyGroup || issue?.start_date) && ( + {displayProperties && displayProperties?.start_date && (showEmptyGroup || issue?.start_date) && ( handleStartDate(date)} @@ -111,7 +111,7 @@ export const KanBanProperties: FC = observer((props) => { )} {/* target/due date */} - {display_properties && display_properties?.due_date && (showEmptyGroup || issue?.target_date) && ( + {displayProperties && displayProperties?.due_date && (showEmptyGroup || issue?.target_date) && ( handleTargetDate(date)} @@ -121,7 +121,7 @@ export const KanBanProperties: FC = observer((props) => { )} {/* estimates */} - {display_properties && display_properties?.estimate && ( + {displayProperties && displayProperties?.estimate && ( = observer((props) => { {/* extra render properties */} {/* sub-issues */} - {display_properties && display_properties?.sub_issue_count && ( + {displayProperties && displayProperties?.sub_issue_count && (
@@ -143,7 +143,7 @@ export const KanBanProperties: FC = observer((props) => { )} {/* attachments */} - {display_properties && display_properties?.attachment_count && ( + {displayProperties && displayProperties?.attachment_count && (
@@ -153,7 +153,7 @@ export const KanBanProperties: FC = observer((props) => { )} {/* link */} - {display_properties && display_properties?.link && ( + {displayProperties && displayProperties?.link && (
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 63c2f128194..396f7b50835 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 @@ -19,13 +19,15 @@ export const ArchivedIssueListLayout: FC = observer(() => { const { project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, archivedIssues: archivedIssueStore, archivedIssueFilters: archivedIssueFiltersStore, } = useMobxStore(); // derived values const issues = archivedIssueStore.getIssues; - const display_properties = archivedIssueFiltersStore?.userDisplayProperties || null; + const displayProperties = archivedIssueFiltersStore?.userDisplayProperties || null; const group_by: string | null = archivedIssueFiltersStore?.userDisplayFilters?.group_by || null; const handleIssues = (group_by: string | null, issue: IIssue, action: "delete" | "update") => { @@ -38,10 +40,9 @@ export const ArchivedIssueListLayout: FC = observer(() => { const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; const estimates = @@ -59,12 +60,12 @@ export const ArchivedIssueListLayout: FC = observer(() => { quickActions={(group_by, issue) => ( handleIssues(group_by, issue, "delete")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} states={states} stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members?.map((m) => m.member) ?? null} + members={projectMembers?.map((m) => m.member) ?? null} projects={projects} estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> 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 9a67b91c1f3..608607ab811 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -21,6 +21,8 @@ export const CycleListLayout: React.FC = observer(() => { // store const { project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, issueFilter: issueFilterStore, cycleIssue: cycleIssueStore, issueDetail: issueDetailStore, @@ -31,7 +33,7 @@ export const CycleListLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - const display_properties = issueFilterStore?.userDisplayProperties || null; + const displayProperties = issueFilterStore?.userDisplayProperties || null; const handleIssues = useCallback( (group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { @@ -55,10 +57,9 @@ export const CycleListLayout: React.FC = observer(() => { [cycleIssueStore, issueDetailStore, cycleId, workspaceSlug] ); - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; const estimates = @@ -80,12 +81,12 @@ export const CycleListLayout: React.FC = observer(() => { handleRemoveFromCycle={async () => handleIssues(group_by, issue, "remove")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} states={states} stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members?.map((m) => m.member) ?? null} + members={projectMembers?.map((m) => m.member) ?? null} projects={projects} estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> 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 fd748bc6cdc..1619112f196 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -21,6 +21,8 @@ export const ModuleListLayout: React.FC = observer(() => { const { project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, issueFilter: issueFilterStore, moduleIssue: moduleIssueStore, issueDetail: issueDetailStore, @@ -31,7 +33,7 @@ export const ModuleListLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - const display_properties = issueFilterStore?.userDisplayProperties || null; + const displayProperties = issueFilterStore?.userDisplayProperties || null; const handleIssues = useCallback( (group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { @@ -55,10 +57,9 @@ export const ModuleListLayout: React.FC = observer(() => { [moduleIssueStore, issueDetailStore, moduleId, workspaceSlug] ); - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; const estimates = @@ -80,12 +81,12 @@ export const ModuleListLayout: React.FC = observer(() => { handleRemoveFromModule={async () => handleIssues(group_by, issue, "remove")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} states={states} stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members?.map((m) => m.member) ?? null} + members={projectMembers?.map((m) => m.member) ?? null} projects={projects} estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> 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 9e4937ffdb1..bfb7278432c 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 @@ -16,7 +16,9 @@ export interface IProfileIssuesListLayout {} export const ProfileIssuesListLayout: FC = observer(() => { const { workspace: workspaceStore, + projectState: projectStateStore, project: projectStore, + projectMember: { projectMembers }, profileIssueFilters: profileIssueFiltersStore, profileIssues: profileIssuesStore, issueDetail: issueDetailStore, @@ -29,7 +31,7 @@ export const ProfileIssuesListLayout: FC = observer(() => { const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; - const display_properties = profileIssueFiltersStore?.userDisplayProperties || null; + const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null; const handleIssues = useCallback( (group_by: string | null, issue: IIssue, action: "update" | "delete") => { @@ -44,10 +46,9 @@ export const ProfileIssuesListLayout: FC = observer(() => { [profileIssuesStore, issueDetailStore, workspaceSlug] ); - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = workspaceStore.workspaceLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = projectStore?.workspaceProjects || null; @@ -64,12 +65,12 @@ export const ProfileIssuesListLayout: FC = observer(() => { handleUpdate={async (data) => handleIssues(group_by, data, "update")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} states={states} stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members?.map((m) => m.member) ?? null} + members={projectMembers?.map((m) => m.member) ?? null} projects={projects} estimates={null} /> 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 16ce940b52c..beec1400890 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -6,6 +6,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; // components import { List } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; +import { Spinner } from "@plane/ui"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types @@ -19,6 +20,8 @@ export const ListLayout: FC = observer(() => { // store const { project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, issue: issueStore, issueDetail: issueDetailStore, issueFilter: issueFilterStore, @@ -29,7 +32,7 @@ export const ListLayout: FC = observer(() => { const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; const group_by: string | null = userDisplayFilters?.group_by || null; - const display_properties = issueFilterStore?.userDisplayProperties || null; + const displayProperties = issueFilterStore?.userDisplayProperties || null; const handleIssues = useCallback( (group_by: string | null, issue: IIssue, action: "update" | "delete") => { @@ -44,10 +47,9 @@ export const ListLayout: FC = observer(() => { [issueStore, issueDetailStore, workspaceSlug] ); - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; const estimates = @@ -56,29 +58,37 @@ export const ListLayout: FC = observer(() => { : null; return ( -
- ( - handleIssues(group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(group_by, data, "update")} + <> + {issueStore.loader ? ( +
+ +
+ ) : ( +
+ ( + handleIssues(group_by, issue, "delete")} + handleUpdate={async (data) => handleIssues(group_by, data, "update")} + /> + )} + displayProperties={displayProperties} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={projectMembers?.map((m) => m.member) ?? null} + projects={projects} + enableQuickIssueCreate + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} + showEmptyGroup={userDisplayFilters.show_empty_groups} /> - )} - display_properties={display_properties} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - enableQuickIssueCreate - estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} - showEmptyGroup={userDisplayFilters.show_empty_groups} - /> -
+
+ )} + ); }); 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 85b8177b3d1..a5dc76352ca 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 @@ -11,7 +11,12 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface IViewListLayout {} export const ProjectViewListLayout: React.FC = observer(() => { - const { project: projectStore, issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); + const { + project: projectStore, + issue: issueStore, + issueFilter: issueFilterStore, + projectState: projectStateStore, + }: RootStore = useMobxStore(); const issues = issueStore?.getIssues; @@ -23,12 +28,11 @@ export const ProjectViewListLayout: React.FC = observer(() => { issueStore.updateIssueStructure(group_by, null, issue); }; - const states = projectStore?.projectStates || null; + const states = projectStateStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; - const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStore?.projectStates || null; + const projects = projectStateStore?.projectStates || null; const estimates = null; return null; diff --git a/web/components/issues/issue-layouts/properties/assignee.tsx b/web/components/issues/issue-layouts/properties/assignee.tsx index b436e274f9b..f7a4f322b20 100644 --- a/web/components/issues/issue-layouts/properties/assignee.tsx +++ b/web/components/issues/issue-layouts/properties/assignee.tsx @@ -39,27 +39,33 @@ export const IssuePropertyAssignee: React.FC = observer( multiple = false, noLabelBorder = false, } = props; - - const { workspace: workspaceStore, project: projectStore } = useMobxStore(); + // store + const { + workspace: workspaceStore, + project: projectStore, + workspaceMember: { workspaceMembers, fetchWorkspaceMembers }, + } = useMobxStore(); const workspaceSlug = workspaceStore?.workspaceSlug; - + // states const [query, setQuery] = useState(""); - const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const [isLoading, setIsLoading] = useState(false); - const projectMembers = projectId ? projectStore?.members?.[projectId] : undefined; + // const fetchProjectMembers = () => { + // setIsLoading(true); + // if (workspaceSlug && projectId) + // workspaceSlug && + // projectId && + // projectStore.fetchProjectMembers(workspaceSlug, projectId).then(() => setIsLoading(false)); + // }; - const fetchProjectMembers = () => { + const getWorkspaceMembers = () => { setIsLoading(true); - if (workspaceSlug && projectId) - workspaceSlug && - projectId && - projectStore.fetchProjectMembers(workspaceSlug, projectId).then(() => setIsLoading(false)); + if (workspaceSlug) workspaceSlug && fetchWorkspaceMembers(workspaceSlug).then(() => setIsLoading(false)); }; - const options = (projectMembers ?? [])?.map((member) => ({ + const options = (workspaceMembers ?? [])?.map((member) => ({ value: member.member.id, query: member.member.display_name, content: ( @@ -73,27 +79,38 @@ export const IssuePropertyAssignee: React.FC = observer( const filteredOptions = query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + const getTooltipContent = (): string => { + if (!value || value.length === 0) return "No Assignee"; + + // if multiple assignees + if (Array.isArray(value)) { + const assignees = workspaceMembers?.filter((m) => value.includes(m.member.id)); + + if (!assignees || assignees.length === 0) return "No Assignee"; + + // if only one assignee in list + if (assignees.length === 1) { + return "1 assignee"; + } else return `${assignees.length} assignees`; + } + + // if single assignee + const assignee = workspaceMembers?.find((m) => m.member.id === value)?.member; + + if (!assignee) return "No Assignee"; + + // if assignee not null & not list + return "1 assignee"; + }; + const label = ( - 0 - ? (projectMembers ? projectMembers : []) - ?.filter((m) => value.includes(m.member.display_name)) - .map((m) => m.member.display_name) - .join(", ") - : "No Assignee" - } - position="top" - > +
{value && value.length > 0 && Array.isArray(value) ? ( {value.map((assigneeId) => { - const member = projectMembers?.find((m) => m.member.id === assigneeId)?.member; - + const member = workspaceMembers?.find((m) => m.member.id === assigneeId)?.member; if (!member) return null; - return ; })} @@ -134,7 +151,7 @@ export const IssuePropertyAssignee: React.FC = observer( className={`flex items-center justify-between gap-1 w-full text-xs ${ disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80" } ${buttonClassName}`} - onClick={() => !projectMembers && fetchProjectMembers()} + onClick={() => !workspaceMembers && getWorkspaceMembers()} > {label} {!hideDropdownArrow && !disabled &&
-
- {workspace.id === activeWorkspace?.id && ( - - - +
+ + {workspace?.logo && workspace.logo !== "" ? ( + Workspace Logo + ) : ( + workspace?.name?.charAt(0) ?? "..." + )} + + +
+ {workspace.name} +
+
+ {workspace.id === activeWorkspace?.id && ( + + + + )} + )} - - )} + + )) + ) : ( +

No workspace found!

+ )} + { + router.push("/create-workspace"); + }} + className="flex w-full items-center gap-2 px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80" + > + + Create Workspace - )) +
) : ( -

No workspace found!

+
+ + + + +
)} +
+
+ {userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => ( + + + {link.name} + + + ))} +
+
{ - router.push("/create-workspace"); - }} - className="flex w-full items-center gap-2 px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80" + className="flex w-full items-center justify-start rounded px-2 py-1 text-sm text-red-600 hover:bg-custom-sidebar-background-80" + onClick={handleSignOut} > - - Create Workspace + Sign out
- ) : ( -
- - - - -
- )} -
-
- {userLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map((link, index) => ( - - - {link.name} - - - ))} -
-
- - Sign out - -
- - + + + + )} - {!themeStore.sidebarCollapsed && ( + {!sidebarCollapsed && ( - + { border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none" >
- {user?.email} - {profileLinks(workspaceSlug?.toString() ?? "", user?.id ?? "").map((link, index) => ( + {currentUser?.email} + {profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => ( diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index b09496cc468..9ae9ccde8b7 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -8,14 +8,18 @@ import useLocalStorage from "hooks/use-local-storage"; import { CreateUpdateDraftIssueModal } from "components/issues"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; +import { observer } from "mobx-react-lite"; -export const WorkspaceSidebarQuickAction = () => { - const store: any = useMobxStore(); - +export const WorkspaceSidebarQuickAction = observer(() => { + // states const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); + const { theme: themeStore, commandPalette: commandPaletteStore } = useMobxStore(); + const { storedValue, clearValue } = useLocalStorage("draftedIssue", JSON.stringify({})); + const isSidebarCollapsed = themeStore.sidebarCollapsed; + return ( <> {
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( <> -
+
); -}; +}); diff --git a/web/components/workspace/single-invitation.tsx b/web/components/workspace/single-invitation.tsx index 0621c64b3f2..4db6c5f188c 100644 --- a/web/components/workspace/single-invitation.tsx +++ b/web/components/workspace/single-invitation.tsx @@ -9,11 +9,7 @@ type Props = { handleInvitation: any; }; -const SingleInvitation: React.FC = ({ - invitation, - invitationsRespond, - handleInvitation, -}) => ( +const SingleInvitation: React.FC = ({ invitation, invitationsRespond, handleInvitation }) => (
  • diff --git a/web/components/workspace/views/view-list-item.tsx b/web/components/workspace/views/view-list-item.tsx index b6b62a4288b..867ce3b5efc 100644 --- a/web/components/workspace/views/view-list-item.tsx +++ b/web/components/workspace/views/view-list-item.tsx @@ -1,14 +1,12 @@ +import { useState } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; import { observer } from "mobx-react-lite"; -import { useState } from "react"; - +import { Pencil, Trash2 } from "lucide-react"; // components import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; // ui -import { CustomMenu } from "@plane/ui"; -// icons -import { Pencil, Sparkles, Trash2 } from "lucide-react"; +import { CustomMenu, PhotoFilterIcon } from "@plane/ui"; // helpers import { truncateText } from "helpers/string.helper"; import { calculateTotalFilters } from "helpers/filter.helper"; @@ -38,7 +36,7 @@ export const GlobalViewListItem: React.FC = observer((props) => {
    - +

    {truncateText(view.name, 75)}

    diff --git a/web/constants/crisp.tsx b/web/constants/crisp.tsx index d89aaea3171..90dfda7c793 100644 --- a/web/constants/crisp.tsx +++ b/web/constants/crisp.tsx @@ -11,8 +11,9 @@ declare global { } const Crisp = observer(() => { - const { user: userStore } = useMobxStore(); - const { currentUser } = userStore; + const { + user: { currentUser }, + } = useMobxStore(); const validateCurrentUser = useCallback(() => { if (currentUser) return currentUser.email; diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 144cad190c1..5ba0f48a043 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -1,4 +1,3 @@ -import { v4 as uuidv4 } from "uuid"; // icons import { Calendar, GanttChartSquare, Kanban, List, Sheet } from "lucide-react"; // types @@ -12,9 +11,6 @@ import { TIssuePriorities, TIssueTypeFilters, TStateGroups, - IIssue, - IProject, - IWorkspace, } from "types"; export const ISSUE_PRIORITIES: { @@ -420,72 +416,3 @@ export const groupReactionEmojis = (reactions: any) => { return _groupedEmojis; }; - -/** - * - * @param workspaceDetail workspace detail to be added in the issue payload - * @param projectDetail project detail to be added in the issue payload - * @param formData partial issue data from the form. This will override the default values - * @returns full issue payload with some default values - */ - -export const createIssuePayload: ( - workspaceDetail: IWorkspace, - projectDetail: IProject, - formData: Partial -) => IIssue = (workspaceDetail: IWorkspace, projectDetail: IProject, formData: Partial) => { - const payload = { - archived_at: null, - assignees: [], - assignee_details: [], - attachment_count: 0, - attachments: [], - issue_relations: [], - related_issues: [], - bridge_id: null, - completed_at: new Date(), - created_at: "", - created_by: "", - cycle: null, - cycle_id: null, - cycle_detail: null, - description: {}, - description_html: "", - description_stripped: "", - estimate_point: null, - issue_cycle: null, - issue_link: [], - issue_module: null, - labels: [], - label_details: [], - is_draft: false, - links_list: [], - link_count: 0, - module: null, - module_id: null, - name: "", - parent: null, - parent_detail: null, - priority: "none", - project: projectDetail.id, - project_detail: projectDetail, - sequence_id: 0, - sort_order: 0, - sprints: null, - start_date: null, - state: projectDetail.default_state, - state_detail: {} as any, - sub_issues_count: 0, - target_date: null, - updated_at: "", - updated_by: "", - workspace: workspaceDetail.id, - workspace_detail: workspaceDetail, - id: uuidv4(), - tempId: uuidv4(), - // to be overridden by the form data - ...formData, - } as IIssue; - - return payload; -}; diff --git a/web/constants/workspace.ts b/web/constants/workspace.ts index 084bdc68af4..e442bf242e4 100644 --- a/web/constants/workspace.ts +++ b/web/constants/workspace.ts @@ -1,6 +1,6 @@ // services images import GithubLogo from "public/services/github.png"; -import JiraLogo from "public/services/jira.png"; +import JiraLogo from "public/services/jira.svg"; import CSVLogo from "public/services/csv.svg"; import ExcelLogo from "public/services/excel.svg"; import JSONLogo from "public/services/json.svg"; @@ -28,7 +28,7 @@ export const USER_ROLES = [ { value: "Other", label: "Other" }, ]; -export const IMPORTERS_EXPORTERS_LIST = [ +export const IMPORTERS_LIST = [ { provider: "github", type: "import", diff --git a/web/contexts/issue-view.context.tsx b/web/contexts/issue-view.context.tsx index 7807523e445..c64dcf194a1 100644 --- a/web/contexts/issue-view.context.tsx +++ b/web/contexts/issue-view.context.tsx @@ -4,7 +4,7 @@ import useSWR, { mutate } from "swr"; // components import ToastAlert from "components/toast-alert"; // services -import { ProjectService } from "services/project"; +import { ProjectService, ProjectMemberService } from "services/project"; import { CycleService } from "services/cycle.service"; import { ModuleService } from "services/module.service"; import { ViewService } from "services/view.service"; @@ -16,6 +16,7 @@ import { IIssueFilterOptions, IProjectMember, IUser, IIssueDisplayFilterOptions, import { CYCLE_DETAILS, MODULE_DETAILS, USER_PROJECT_VIEW, VIEW_DETAILS } from "constants/fetch-keys"; const projectService = new ProjectService(); +const projectMemberService = new ProjectMemberService(); const cycleService = new CycleService(); const moduleService = new ModuleService(); const viewService = new ViewService(); @@ -218,7 +219,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = const { data: myViewProps, mutate: mutateMyViewProps } = useSWR( workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMemberMe(workspaceSlug as string, projectId as string) + ? () => projectMemberService.projectMemberMe(workspaceSlug as string, projectId as string) : null ); diff --git a/web/contexts/profile-issues-context.tsx b/web/contexts/profile-issues-context.tsx index 44c17b50a16..f7586bdebac 100644 --- a/web/contexts/profile-issues-context.tsx +++ b/web/contexts/profile-issues-context.tsx @@ -3,12 +3,7 @@ import { createContext, useCallback, useReducer } from "react"; // components import ToastAlert from "components/toast-alert"; // types -import { - IIssueFilterOptions, - Properties, - IWorkspaceViewProps, - IIssueDisplayFilterOptions, -} from "types"; +import { IIssueFilterOptions, Properties, IWorkspaceViewProps, IIssueDisplayFilterOptions } from "types"; export const profileIssuesContext = createContext({} as ContextType); @@ -117,9 +112,7 @@ export const reducer: ReducerFunctionType = (state, action) => { } }; -export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { +export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); const setDisplayFilters = useCallback( @@ -133,11 +126,7 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode }, }); - if ( - displayFilter.layout && - displayFilter.layout === "kanban" && - state.display_filters?.group_by === null - ) { + if (displayFilter.layout && displayFilter.layout === "kanban" && state.display_filters?.group_by === null) { dispatch({ type: "SET_DISPLAY_FILTERS", payload: { @@ -154,8 +143,7 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode const setFilters = useCallback( (property: Partial) => { Object.keys(property).forEach((key) => { - if (property[key as keyof typeof property]?.length === 0) - property[key as keyof typeof property] = null; + if (property[key as keyof typeof property]?.length === 0) property[key as keyof typeof property] = null; }); dispatch({ diff --git a/web/contexts/toast.context.tsx b/web/contexts/toast.context.tsx index 69adf0e9299..30e100b209b 100644 --- a/web/contexts/toast.context.tsx +++ b/web/contexts/toast.context.tsx @@ -72,11 +72,7 @@ export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ }, []); const setToastAlert = useCallback( - (data: { - title: string; - type?: "success" | "error" | "warning" | "info"; - message?: string; - }) => { + (data: { title: string; type?: "success" | "error" | "warning" | "info"; message?: string }) => { const id = uuid(); const { title, type, message } = data; dispatch({ diff --git a/web/helpers/array.helper.ts b/web/helpers/array.helper.ts index a682b0a1c12..a55ad8fd90c 100644 --- a/web/helpers/array.helper.ts +++ b/web/helpers/array.helper.ts @@ -7,11 +7,7 @@ export const groupBy = (array: any[], key: string) => { }, {}); }; -export const orderArrayBy = ( - orgArray: any[], - key: string, - ordering: "ascending" | "descending" = "ascending" -) => { +export const orderArrayBy = (orgArray: any[], key: string, ordering: "ascending" | "descending" = "ascending") => { if (!orgArray || !Array.isArray(orgArray) || orgArray.length === 0) return []; const array = [...orgArray]; @@ -53,3 +49,28 @@ export const checkIfArraysHaveSameElements = (arr1: any[] | null, arr2: any[] | return arr1.length === arr2.length && arr1.every((e) => arr2.includes(e)); }; + +type GroupedItems = { [key: string]: T[] }; + +export const groupByField = (array: T[], field: keyof T): GroupedItems => + array.reduce((grouped: GroupedItems, item: T) => { + const key = String(item[field]); + grouped[key] = (grouped[key] || []).concat(item); + return grouped; + }, {}); + +export const sortByField = (array: any[], field: string): any[] => + array.sort((a, b) => (a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0)); + +export const orderGroupedDataByField = (groupedData: GroupedItems, orderBy: keyof T): GroupedItems => { + for (const key in groupedData) { + if (groupedData.hasOwnProperty(key)) { + groupedData[key] = groupedData[key].sort((a, b) => { + if (a[orderBy] < b[orderBy]) return -1; + if (a[orderBy] > b[orderBy]) return 1; + return 0; + }); + } + } + return groupedData; +}; diff --git a/web/helpers/attachment.helper.ts b/web/helpers/attachment.helper.ts index 24cfb2c4937..67e98906323 100644 --- a/web/helpers/attachment.helper.ts +++ b/web/helpers/attachment.helper.ts @@ -1,5 +1,4 @@ -export const getFileExtension = (filename: string) => - filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); +export const getFileExtension = (filename: string) => filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); export const getFileName = (fileName: string) => { const dotIndex = fileName.lastIndexOf("."); diff --git a/web/helpers/common.helper.ts b/web/helpers/common.helper.ts index 7fb8896dee8..ed199fed41d 100644 --- a/web/helpers/common.helper.ts +++ b/web/helpers/common.helper.ts @@ -17,6 +17,4 @@ export const debounce = (func: any, wait: number, immediate: boolean = false) => }; }; -export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL - ? process.env.NEXT_PUBLIC_API_BASE_URL - : ""; +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : ""; diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts index dced747f9ce..b49cb88148a 100644 --- a/web/helpers/date-time.helper.ts +++ b/web/helpers/date-time.helper.ts @@ -133,18 +133,13 @@ export const formatDateDistance = (date: string | Date) => { export const getDateRangeStatus = (startDate: string | null | undefined, endDate: string | null | undefined) => { if (!startDate || !endDate) return "draft"; - const today = renderDateFormat(new Date()); - const now = new Date(today); + const now = new Date(); const start = new Date(startDate); const end = new Date(endDate); - if (start <= now && end >= now) { - return "current"; - } else if (start > now) { - return "upcoming"; - } else { - return "completed"; - } + if (start <= now && end >= now) return "current"; + else if (start > now) return "upcoming"; + else return "completed"; }; export const renderShortDateWithYearFormat = (date: string | Date, placeholder?: string) => { @@ -373,7 +368,7 @@ export const findTotalDaysInRange = (startDate: Date | string, endDate: Date | s // find number of days between startDate and endDate const diffInTime = endDate.getTime() - startDate.getTime(); - const diffInDays = diffInTime / (1000 * 3600 * 24); + const diffInDays = Math.floor(diffInTime / (1000 * 3600 * 24)); // if inclusive is true, add 1 to diffInDays if (inclusive) return diffInDays + 1; diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 38b830071cc..026211634ed 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -41,13 +41,16 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: reactions: any, key: string ) => { - const groupedReactions = reactions.reduce((acc: any, reaction: any) => { - if (!acc[reaction[key]]) { - acc[reaction[key]] = []; - } - acc[reaction[key]].push(reaction); - return acc; - }, {} as { [key: string]: any[] }); + const groupedReactions = reactions.reduce( + (acc: any, reaction: any) => { + if (!acc[reaction[key]]) { + acc[reaction[key]] = []; + } + acc[reaction[key]].push(reaction); + return acc; + }, + {} as { [key: string]: any[] } + ); return groupedReactions; }; diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 6f4381dec18..39424c76e6d 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -1,7 +1,16 @@ +import { v4 as uuidv4 } from "uuid"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { IIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "types"; +import { + IIssue, + TIssueGroupByOptions, + TIssueLayouts, + TIssueOrderByOptions, + TIssueParams, + IProject, + IWorkspace, +} from "types"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; @@ -109,3 +118,82 @@ export const handleIssueQueryParamsByLayout = ( return queryParams; }; + +/** + * + * @description create a full issue payload with some default values. This function also parse the form field + * like assignees, labels, etc. and add them to the payload + * @param workspaceDetail workspace detail to be added in the issue payload + * @param projectDetail project detail to be added in the issue payload + * @param formData partial issue data from the form. This will override the default values + * @returns full issue payload with some default values + */ + +export const createIssuePayload: ( + workspaceDetail: IWorkspace, + projectDetail: IProject, + formData: Partial +) => IIssue = (workspaceDetail: IWorkspace, projectDetail: IProject, formData: Partial) => { + const payload = { + archived_at: null, + assignee_details: [], + attachment_count: 0, + attachments: [], + issue_relations: [], + related_issues: [], + bridge_id: null, + completed_at: new Date(), + created_at: "", + created_by: "", + cycle: null, + cycle_id: null, + cycle_detail: null, + description: {}, + description_html: "", + description_stripped: "", + estimate_point: null, + issue_cycle: null, + issue_link: [], + issue_module: null, + label_details: [], + is_draft: false, + links_list: [], + link_count: 0, + module: null, + module_id: null, + name: "", + parent: null, + parent_detail: null, + priority: "none", + project: projectDetail.id, + project_detail: projectDetail, + sequence_id: 0, + sort_order: 0, + sprints: null, + start_date: null, + state: projectDetail.default_state, + state_detail: {} as any, + sub_issues_count: 0, + target_date: null, + updated_at: "", + updated_by: "", + workspace: workspaceDetail.id, + workspace_detail: workspaceDetail, + id: uuidv4(), + tempId: uuidv4(), + // to be overridden by the form data + ...formData, + assignees: Array.isArray(formData.assignees) + ? formData.assignees + : formData.assignees && formData.assignees !== "none" && formData.assignees !== null + ? [formData.assignees] + : [], + labels: Array.isArray(formData.labels) + ? formData.labels + : formData.labels && formData.labels !== "none" && formData.labels !== null + ? [formData.labels] + : [], + } as IIssue; + + return payload; +}; diff --git a/web/helpers/state.helper.ts b/web/helpers/state.helper.ts index edf740c1065..ef6c3ba77f0 100644 --- a/web/helpers/state.helper.ts +++ b/web/helpers/state.helper.ts @@ -1,27 +1,7 @@ // types -import { IState, IStateResponse } from "types"; +import { IStateResponse } from "types"; -export const orderStateGroups = ( - unorderedStateGroups: IStateResponse | undefined -): IStateResponse | undefined => { +export const orderStateGroups = (unorderedStateGroups: IStateResponse | undefined): IStateResponse | undefined => { if (!unorderedStateGroups) return undefined; - - return Object.assign( - { backlog: [], unstarted: [], started: [], completed: [], cancelled: [] }, - unorderedStateGroups - ); -}; - -export const getStatesList = (stateGroups: IStateResponse | undefined): IState[] | undefined => { - if (!stateGroups) return undefined; - - // order the unordered state groups first - const orderedStateGroups = orderStateGroups(stateGroups); - - if (!orderedStateGroups) return undefined; - - // extract states from the groups and return them - return Object.keys(orderedStateGroups) - .map((group) => [...orderedStateGroups[group].map((state: IState) => state)]) - .flat(); + return Object.assign({ backlog: [], unstarted: [], started: [], completed: [], cancelled: [] }, unorderedStateGroups); }; diff --git a/web/hooks/use-editor-suggestions.tsx b/web/hooks/use-editor-suggestions.tsx index 6247989f0ea..337fc5f0a0e 100644 --- a/web/hooks/use-editor-suggestions.tsx +++ b/web/hooks/use-editor-suggestions.tsx @@ -1,9 +1,7 @@ -import { IMentionHighlight, IMentionSuggestion } from "@plane/rich-text-editor"; -import useUser from "./use-user"; import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; -const useEditorSuggestions = (_workspaceSlug: string | undefined, _projectId: string | undefined) => { +const useEditorSuggestions = () => { const { mentionsStore }: RootStore = useMobxStore(); return { diff --git a/web/hooks/use-integration-popup.tsx b/web/hooks/use-integration-popup.tsx index 58cfbc009b8..fb9aab2238a 100644 --- a/web/hooks/use-integration-popup.tsx +++ b/web/hooks/use-integration-popup.tsx @@ -1,23 +1,26 @@ import { useRef, useState } from "react"; - import { useRouter } from "next/router"; -const useIntegrationPopup = (provider: string | undefined, stateParams?: string) => { +const useIntegrationPopup = ({ + provider, + stateParams, + github_app_name, + slack_client_id, +}: { + provider: string | undefined; + stateParams?: string; + github_app_name?: string; + slack_client_id?: string; +}) => { const [authLoader, setAuthLoader] = useState(false); const router = useRouter(); const { workspaceSlug, projectId } = router.query; const providerUrls: { [key: string]: string } = { - github: `https://github.com/apps/${ - process.env.NEXT_PUBLIC_GITHUB_APP_NAME - }/installations/new?state=${workspaceSlug?.toString()}`, - slack: `https://slack.com/oauth/v2/authorize?scope=chat:write,im:history,im:write,links:read,links:write,users:read,users:read.email&user_scope=&&client_id=${ - process.env.NEXT_PUBLIC_SLACK_CLIENT_ID - }&state=${workspaceSlug?.toString()}`, - slackChannel: `https://slack.com/oauth/v2/authorize?scope=incoming-webhook&client_id=${ - process.env.NEXT_PUBLIC_SLACK_CLIENT_ID - }&state=${workspaceSlug?.toString()},${projectId?.toString()}${ + github: `https://github.com/apps/${github_app_name}/installations/new?state=${workspaceSlug?.toString()}`, + slack: `https://slack.com/oauth/v2/authorize?scope=chat:write,im:history,im:write,links:read,links:write,users:read,users:read.email&user_scope=&&client_id=${slack_client_id}&state=${workspaceSlug?.toString()}`, + slackChannel: `https://slack.com/oauth/v2/authorize?scope=incoming-webhook&client_id=${slack_client_id}&state=${workspaceSlug?.toString()},${projectId?.toString()}${ stateParams ? "," + stateParams : "" }`, }; diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index e3eca37ff12..225e92f2a36 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -19,12 +19,15 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { children } = props; // store const { - user: userStore, - project: projectStore, - cycle: cycleStore, - module: moduleStore, - projectViews: projectViewsStore, - inbox: inboxStore, + user: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject }, + project: { fetchProjectDetails, fetchProjectLabels, fetchProjectEstimates, workspaceProjects }, + projectMember: { fetchProjectMembers }, + projectState: { fetchProjectStates }, + cycle: { fetchCycles }, + module: { fetchModules }, + projectViews: { fetchAllViews }, + inbox: { fetchInboxesList, isInboxEnabled }, + commandPalette: { toggleCreateProjectModal }, } = useMobxStore(); // router const router = useRouter(); @@ -33,70 +36,54 @@ export const ProjectAuthWrapper: FC = observer((props) => { // fetching project details useSWR( workspaceSlug && projectId ? `PROJECT_DETAILS_${workspaceSlug.toString()}_${projectId.toString()}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null ); // fetching user project member information useSWR( workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => userStore.fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project labels useSWR( workspaceSlug && projectId ? `PROJECT_LABELS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project members useSWR( workspaceSlug && projectId ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project states useSWR( workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project estimates useSWR( workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectEstimates(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project cycles useSWR( workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => cycleStore.fetchCycles(workspaceSlug.toString(), projectId.toString(), "all") - : null + workspaceSlug && projectId ? () => fetchCycles(workspaceSlug.toString(), projectId.toString(), "all") : null ); // fetching project modules useSWR( workspaceSlug && projectId ? `PROJECT_MODULES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => moduleStore.fetchModules(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId ? () => fetchModules(workspaceSlug.toString(), projectId.toString()) : null ); // fetching project views useSWR( workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId - ? () => projectViewsStore.fetchAllViews(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? () => fetchAllViews(workspaceSlug.toString(), projectId.toString()) : null ); // TODO: fetching project pages // fetching project inboxes if inbox is enabled useSWR( - workspaceSlug && projectId && inboxStore.isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId && inboxStore.isInboxEnabled - ? () => inboxStore.fetchInboxesList(workspaceSlug.toString(), projectId.toString()) + workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null, + workspaceSlug && projectId && isInboxEnabled + ? () => fetchInboxesList(workspaceSlug.toString(), projectId.toString()) : null, { revalidateOnFocus: false, @@ -104,11 +91,11 @@ export const ProjectAuthWrapper: FC = observer((props) => { } ); - const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; + const projectsList = workspaceSlug ? workspaceProjects : null; const projectExists = projectId ? projectsList?.find((project) => project.id === projectId.toString()) : null; // check if the project member apis is loading - if (!userStore.projectMemberInfo && projectId && userStore.hasPermissionToProject[projectId.toString()] === null) + if (!projectMemberInfo && projectId && hasPermissionToProject[projectId.toString()] === null) return (
    @@ -118,11 +105,10 @@ export const ProjectAuthWrapper: FC = observer((props) => { ); // check if the user don't have permission to access the project - if (projectExists && projectId && userStore.hasPermissionToProject[projectId.toString()] === false) - return ; + if (projectExists && projectId && hasPermissionToProject[projectId.toString()] === false) return ; // check if the project info is not found. - if (!projectExists && projectId && userStore.hasPermissionToProject[projectId.toString()] === false) + if (!projectExists && projectId && hasPermissionToProject[projectId.toString()] === false) return (
    = observer((props) => { image={emptyProject} primaryButton={{ text: "Create Project", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); - }, + onClick: () => toggleCreateProjectModal(true), }} />
    diff --git a/web/layouts/auth-layout/user-wrapper.tsx b/web/layouts/auth-layout/user-wrapper.tsx index 596612153f0..6072f167343 100644 --- a/web/layouts/auth-layout/user-wrapper.tsx +++ b/web/layouts/auth-layout/user-wrapper.tsx @@ -13,19 +13,22 @@ export interface IUserAuthWrapper { export const UserAuthWrapper: FC = (props) => { const { children } = props; // store - const { user: userStore, workspace: workspaceStore } = useMobxStore(); + const { + user: { fetchCurrentUser, fetchCurrentUserSettings }, + workspace: { fetchWorkspaces }, + } = useMobxStore(); // router const router = useRouter(); // fetching user information - const { data: currentUser, error } = useSWR("CURRENT_USER_DETAILS", () => userStore.fetchCurrentUser(), { + const { data: currentUser, error } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), { shouldRetryOnError: false, }); // fetching user settings - useSWR("CURRENT_USER_SETTINGS", () => userStore.fetchCurrentUserSettings(), { + useSWR("CURRENT_USER_SETTINGS", () => fetchCurrentUserSettings(), { shouldRetryOnError: false, }); // fetching all workspaces - useSWR(`USER_WORKSPACES_LIST`, () => workspaceStore.fetchWorkspaces(), { + useSWR(`USER_WORKSPACES_LIST`, () => fetchWorkspaces(), { shouldRetryOnError: false, }); diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index 376fe2e2813..3cfe4e7c8ff 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -15,30 +15,35 @@ export interface IWorkspaceAuthWrapper { export const WorkspaceAuthWrapper: FC = observer((props) => { const { children } = props; // store - const { user: userStore, project: projectStore, workspace: workspaceStore } = useMobxStore(); - const { currentWorkspaceMemberInfo, hasPermissionToCurrentWorkspace } = userStore; + const { + user: { currentWorkspaceMemberInfo, hasPermissionToCurrentWorkspace, fetchUserWorkspaceInfo }, + project: { fetchProjects }, + workspace: { fetchWorkspaceLabels }, + workspaceMember: { fetchWorkspaceMembers }, + } = useMobxStore(); + // router const router = useRouter(); const { workspaceSlug } = router.query; // fetching user workspace information useSWR( workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null, - workspaceSlug ? () => userStore.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchUserWorkspaceInfo(workspaceSlug.toString()) : null ); // fetching workspace projects useSWR( workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null, - workspaceSlug ? () => projectStore.fetchProjects(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null ); // fetch workspace members useSWR( workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, - workspaceSlug ? () => workspaceStore.fetchWorkspaceMembers(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null ); // fetch workspace labels useSWR( workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null, - workspaceSlug ? () => workspaceStore.fetchWorkspaceLabels(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null ); // while data is being loaded diff --git a/web/lib/auth.ts b/web/lib/auth.ts index 56cfab9ae78..6f84db402fb 100644 --- a/web/lib/auth.ts +++ b/web/lib/auth.ts @@ -76,15 +76,12 @@ export const requiredWorkspaceAdmin = async (workspaceSlug: string, cookie?: str let memberDetail: IWorkspaceMember | null = null; try { - const data = await fetch( - `${API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-members/me/`, - { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) + const data = await fetch(`${API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-members/me/`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }) .then((res) => res.json()) .then((data) => data); @@ -143,9 +140,7 @@ export const homePageRedirect = async (cookie?: string) => { }; } - const lastActiveWorkspace = workspaces.find( - (workspace) => workspace.id === user.last_workspace_id - ); + const lastActiveWorkspace = workspaces.find((workspace) => workspace.id === user.last_workspace_id); if (lastActiveWorkspace) { return { diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 780b12d9992..f89aa72c79c 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from "react"; -// next themes +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; import { useTheme } from "next-themes"; +import { useRouter } from "next/router"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -import { useRouter } from "next/router"; +// helpers import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; -import { observer } from "mobx-react-lite"; const MobxStoreInit = observer(() => { // router @@ -13,16 +14,19 @@ const MobxStoreInit = observer(() => { const { workspaceSlug, projectId, cycleId, moduleId, globalViewId, viewId, inboxId } = router.query; // store const { - theme: themeStore, - user: userStore, - workspace: workspaceStore, - project: projectStore, - cycle: cycleStore, - module: moduleStore, - globalViews: globalViewsStore, - projectViews: projectViewsStore, - inbox: inboxStore, + theme: { sidebarCollapsed, toggleSidebar }, + user: { currentUser }, + workspace: { setWorkspaceSlug }, + project: { setProjectId }, + cycle: { setCycleId }, + module: { setModuleId }, + globalViews: { setGlobalViewId }, + projectViews: { setViewId }, + inbox: { setInboxId }, + appConfig: { fetchAppConfig }, } = useMobxStore(); + // fetching application Config + useSWR("APP_CONFIG", () => fetchAppConfig(), { revalidateIfStale: false, revalidateOnFocus: false }); // state const [dom, setDom] = useState(); // theme @@ -34,36 +38,37 @@ const MobxStoreInit = observer(() => { useEffect(() => { const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed"); const localBoolValue = localValue ? (localValue === "true" ? true : false) : false; - if (localValue && themeStore?.sidebarCollapsed === undefined) { - themeStore.toggleSidebar(localBoolValue); + if (localValue && sidebarCollapsed === undefined) { + toggleSidebar(localBoolValue); } - }, [themeStore, userStore, setTheme]); + }, [sidebarCollapsed, currentUser, setTheme, toggleSidebar]); /** * Setting up the theme of the user by fetching it from local storage */ useEffect(() => { - if (!userStore.currentUser) return; + if (!currentUser) return; if (window) { setDom(window.document?.querySelector("[data-theme='custom']")); } - setTheme(userStore.currentUser?.theme?.theme || "system"); - if (userStore.currentUser?.theme?.theme === "custom" && dom) { - applyTheme(userStore.currentUser?.theme?.palette, false); + setTheme(currentUser?.theme?.theme || "system"); + if (currentUser?.theme?.theme === "custom" && dom) { + applyTheme(currentUser?.theme?.palette, false); } else unsetCustomCssVariables(); - }, [userStore.currentUser, setTheme, dom]); + }, [currentUser, setTheme, dom]); /** * Setting router info to the respective stores. */ useEffect(() => { - if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString()); - if (projectId) projectStore.setProjectId(projectId.toString()); - if (cycleId) cycleStore.setCycleId(cycleId.toString()); - if (moduleId) moduleStore.setModuleId(moduleId.toString()); - if (globalViewId) globalViewsStore.setGlobalViewId(globalViewId.toString()); - if (viewId) projectViewsStore.setViewId(viewId.toString()); - if (inboxId) inboxStore.setInboxId(inboxId.toString()); + if (workspaceSlug) setWorkspaceSlug(workspaceSlug.toString()); + + setProjectId(projectId?.toString() ?? null); + setCycleId(cycleId?.toString() ?? null); + setModuleId(moduleId?.toString() ?? null); + setGlobalViewId(globalViewId?.toString() ?? null); + setViewId(viewId?.toString() ?? null); + setInboxId(inboxId?.toString() ?? null); }, [ workspaceSlug, projectId, @@ -72,13 +77,13 @@ const MobxStoreInit = observer(() => { globalViewId, viewId, inboxId, - workspaceStore, - projectStore, - cycleStore, - moduleStore, - globalViewsStore, - projectViewsStore, - inboxStore, + setWorkspaceSlug, + setProjectId, + setCycleId, + setModuleId, + setGlobalViewId, + setViewId, + setInboxId, ]); return <>; diff --git a/web/package.json b/web/package.json index f78d670043b..b2791978fbd 100644 --- a/web/package.json +++ b/web/package.json @@ -57,10 +57,9 @@ "@types/js-cookie": "^3.0.2", "@types/node": "18.0.6", "@types/nprogress": "^0.2.0", - "@types/react": "18.0.15", - "@types/react-beautiful-dnd": "^13.1.2", + "@types/react": "^18.2.35", "@types/react-color": "^3.0.6", - "@types/react-dom": "18.0.6", + "@types/react-dom": "^18.2.14", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.48.2", "@typescript-eslint/parser": "^5.48.2", diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 0faa835ada7..a71904e67d4 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -28,38 +28,36 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store - const { project: projectStore, user: userStore } = useMobxStore(); - - const user = userStore.currentUser; - const projects = workspaceSlug ? projectStore.projects[workspaceSlug?.toString()] : null; + const { + project: { workspaceProjects }, + user: { currentUser }, + commandPalette: { toggleCreateProjectModal }, + } = useMobxStore(); const trackAnalyticsEvent = (tab: string) => { - if (!user) return; - + if (!currentUser) return; const eventPayload = { workspaceSlug: workspaceSlug?.toString(), }; - const eventType = tab === "scope_and_demand" ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" : "WORKSPACE_CUSTOM_ANALYTICS"; - - trackEventService.trackAnalyticsEvent(eventPayload, eventType, user); + trackEventService.trackAnalyticsEvent(eventPayload, eventType, currentUser); }; useEffect(() => { if (!workspaceSlug) return; - if (user && workspaceSlug) + if (currentUser && workspaceSlug) trackEventService.trackAnalyticsEvent( { workspaceSlug: workspaceSlug?.toString() }, "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS", - user + currentUser ); - }, [user, workspaceSlug]); + }, [currentUser, workspaceSlug]); return ( <> - {projects && projects.length > 0 ? ( + {workspaceProjects && workspaceProjects.length > 0 ? (
    @@ -96,12 +94,7 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { primaryButton={{ icon: , text: "New Project", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "p", - }); - document.dispatchEvent(e); - }, + onClick: () => toggleCreateProjectModal(true), }} /> diff --git a/web/pages/[workspaceSlug]/me/profile/activity.tsx b/web/pages/[workspaceSlug]/me/profile/activity.tsx index 3b5e60f59fc..820a6259682 100644 --- a/web/pages/[workspaceSlug]/me/profile/activity.tsx +++ b/web/pages/[workspaceSlug]/me/profile/activity.tsx @@ -37,7 +37,7 @@ const ProfileActivityPage: NextPageWithLayout = () => { <> {userActivity ? (
    -
    +

    Activity

    @@ -153,7 +153,7 @@ const ProfileActivityPage: NextPageWithLayout = () => {
    -
    +
    {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( Plane diff --git a/web/pages/[workspaceSlug]/me/profile/preferences.tsx b/web/pages/[workspaceSlug]/me/profile/preferences.tsx index 842740f7ac4..d08b9da3ca3 100644 --- a/web/pages/[workspaceSlug]/me/profile/preferences.tsx +++ b/web/pages/[workspaceSlug]/me/profile/preferences.tsx @@ -18,11 +18,13 @@ import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; import { NextPageWithLayout } from "types/app"; const ProfilePreferencesPage: NextPageWithLayout = observer(() => { - const { user: userStore } = useMobxStore(); + const { + user: { currentUser, updateCurrentUserTheme }, + } = useMobxStore(); // states const [currentTheme, setCurrentTheme] = useState(null); // computed - const userTheme = userStore.currentUser?.theme; + const userTheme = currentUser?.theme; // hooks const { setTheme } = useTheme(); const { setToastAlert } = useToast(); @@ -38,7 +40,7 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => { const handleThemeChange = (themeOption: I_THEME_OPTION) => { setTheme(themeOption.value); - userStore.updateCurrentUserTheme(themeOption.value).catch(() => { + updateCurrentUserTheme(themeOption.value).catch(() => { setToastAlert({ title: "Failed to Update the theme", type: "error", @@ -48,9 +50,9 @@ const ProfilePreferencesPage: NextPageWithLayout = observer(() => { return ( <> - {userStore.currentUser ? ( + {currentUser ? (
    -
    +

    Preferences

    diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 775cfcd1107..f0f16fa1cea 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -1,19 +1,14 @@ -import { useState, ReactElement } from "react"; +import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { IssueService } from "services/issue"; // hooks import useLocalStorage from "hooks/use-local-storage"; -import useUser from "hooks/use-user"; -import useToast from "hooks/use-toast"; // layouts import { AppLayout } from "layouts/app-layout"; // components import { CycleIssuesHeader } from "components/headers"; -import { ExistingIssuesListModal } from "components/core"; import { CycleDetailsSidebar } from "components/cycles"; import { CycleLayoutRoot } from "components/issues/issue-layouts"; // ui @@ -21,23 +16,14 @@ import { EmptyState } from "components/common"; // assets import emptyCycle from "public/empty-state/cycle.svg"; // types -import { ISearchIssueResponse } from "types"; import { NextPageWithLayout } from "types/app"; -const issueService = new IssueService(); - const CycleDetailPage: NextPageWithLayout = () => { - const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; const { cycle: cycleStore } = useMobxStore(); - const { user } = useUser(); - - const { setToastAlert } = useToast(); - const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; @@ -52,38 +38,8 @@ const CycleDetailPage: NextPageWithLayout = () => { setValue(`${!isSidebarCollapsed}`); }; - // TODO: add this function to bulk add issues to cycle - const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { - if (!workspaceSlug || !projectId) return; - - const payload = { - issues: data.map((i) => i.id), - }; - - await issueService - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); - }); - }; - - const openIssuesListModal = () => { - setCycleIssuesListModal(true); - }; - return ( <> - {/* TODO: Update logic to bulk add issues to a cycle */} - setCycleIssuesListModal(false)} - searchParams={{ cycle: true }} - handleOnSubmit={handleAddIssuesToCycle} - /> {error ? ( { <>
    - +
    {cycleId && !isSidebarCollapsed && (
    { const { currentProjectDetails } = projectStore; // router const router = useRouter(); - const { workspaceSlug, projectId, peekCycle } = router.query as { - workspaceSlug: string; - projectId: string; - peekCycle: string; - }; - // fetching project details - useSWR( - workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null, - workspaceSlug && projectId ? () => projectStore.fetchProjectDetails(workspaceSlug, projectId) : null - ); + const { workspaceSlug, projectId, peekCycle } = router.query; const handleCurrentLayout = useCallback( (_layout: TCycleLayout) => { @@ -83,11 +73,13 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { const cycleView = cycleStore?.cycleView; const cycleLayout = cycleStore?.cycleLayout; + if (!workspaceSlug || !projectId) return null; + return ( <> setCreateModal(false)} /> @@ -163,29 +155,29 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { - {cycleView && cycleLayout && workspaceSlug && projectId && ( + {cycleView && cycleLayout && ( )} - + - {cycleView && cycleLayout && workspaceSlug && projectId && ( + {cycleView && cycleLayout && ( )} @@ -195,9 +187,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { )} @@ -207,9 +199,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { )} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index a7fd7307a1a..a960135b0f0 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -1,18 +1,13 @@ -import { useState, ReactElement } from "react"; +import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { ModuleService } from "services/module.service"; // hooks import useLocalStorage from "hooks/use-local-storage"; -import useToast from "hooks/use-toast"; -import useUser from "hooks/use-user"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { ExistingIssuesListModal } from "components/core"; import { ModuleDetailsSidebar } from "components/modules"; import { ModuleLayoutRoot } from "components/issues"; import { ModuleIssuesHeader } from "components/headers"; @@ -22,21 +17,13 @@ import { EmptyState } from "components/common"; import emptyModule from "public/empty-state/module.svg"; // types import { NextPageWithLayout } from "types/app"; -import { ISearchIssueResponse } from "types"; - -const moduleService = new ModuleService(); const ModuleIssuesPage: NextPageWithLayout = () => { - // states - const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); // router const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; // store const { module: moduleStore } = useMobxStore(); - // hooks - const { user } = useUser(); - const { setToastAlert } = useToast(); // local storage const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; @@ -48,42 +35,12 @@ const ModuleIssuesPage: NextPageWithLayout = () => { : null ); - // TODO: add this function to bulk add issues to cycle - const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { - if (!workspaceSlug || !projectId) return; - - const payload = { - issues: data.map((i) => i.id), - }; - - await moduleService - .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the module. Please try again.", - }) - ); - }; - - const openIssuesListModal = () => { - setModuleIssuesListModal(true); - }; - const toggleSidebar = () => { setValue(`${!isSidebarCollapsed}`); }; return ( <> - {/* TODO: Update logic to bulk add issues to a cycle */} - setModuleIssuesListModal(false)} - searchParams={{ module: true }} - handleOnSubmit={handleAddIssuesToModule} - /> {error ? ( { ) : (
    - +
    {moduleId && !isSidebarCollapsed && (
    { const { data: memberDetails } = useSWR( workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, workspaceSlug && projectId - ? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) + ? () => projectMemberService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) : null ); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx index 0a000e847bb..db070d6c6da 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/automations.tsx @@ -1,8 +1,8 @@ import React, { ReactElement } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; // services -import { ProjectService } from "services/project"; +import { ProjectService, ProjectMemberService } from "services/project"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; @@ -17,10 +17,11 @@ import { ProjectSettingHeader } from "components/headers"; import { NextPageWithLayout } from "types/app"; import { IProject } from "types"; // constant -import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys"; +import { USER_PROJECT_VIEW } from "constants/fetch-keys"; // services const projectService = new ProjectService(); +const projectMemberService = new ProjectMemberService(); const AutomationSettingsPage: NextPageWithLayout = () => { const router = useRouter(); @@ -34,25 +35,13 @@ const AutomationSettingsPage: NextPageWithLayout = () => { const { data: memberDetails } = useSWR( workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, workspaceSlug && projectId - ? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) + ? () => projectMemberService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) : null ); const handleChange = async (formData: Partial) => { if (!workspaceSlug || !projectId || !projectDetails) return; - mutate( - PROJECT_DETAILS(projectId as string), - (prevData) => ({ ...(prevData as IProject), ...formData }), - false - ); - - mutate( - PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), - (prevData) => (prevData ?? []).map((p) => (p.id === projectDetails.id ? { ...p, ...formData } : p)), - false - ); - await projectService .updateProject(workspaceSlug as string, projectId as string, formData, user) .then(() => {}) @@ -69,11 +58,11 @@ const AutomationSettingsPage: NextPageWithLayout = () => { return (
    -
    +

    Automations

    - - + +
    ); }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index ecb8ca02776..2e9597b3d97 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -29,7 +29,7 @@ const FeaturesSettingsPage: NextPageWithLayout = () => { return (
    -
    +

    Features

    diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index cc87c20f438..6895a53ae7e 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -10,7 +10,7 @@ import { NextPageWithLayout } from "types/app"; const StatesSettingsPage: NextPageWithLayout = () => (
    -
    +

    States

    diff --git a/web/pages/[workspaceSlug]/settings/billing.tsx b/web/pages/[workspaceSlug]/settings/billing.tsx index 751b5cfab48..bd7fe3f2f93 100644 --- a/web/pages/[workspaceSlug]/settings/billing.tsx +++ b/web/pages/[workspaceSlug]/settings/billing.tsx @@ -12,7 +12,7 @@ import { NextPageWithLayout } from "types/app"; const BillingSettingsPage: NextPageWithLayout = () => (
    -
    +

    Billing & Plans

    diff --git a/web/pages/[workspaceSlug]/settings/exports.tsx b/web/pages/[workspaceSlug]/settings/exports.tsx index ce0786634cb..fdf3a33930c 100644 --- a/web/pages/[workspaceSlug]/settings/exports.tsx +++ b/web/pages/[workspaceSlug]/settings/exports.tsx @@ -12,7 +12,7 @@ const ExportsPage: NextPageWithLayout = () => ( }>
    -
    +

    Exports

    diff --git a/web/pages/[workspaceSlug]/settings/imports.tsx b/web/pages/[workspaceSlug]/settings/imports.tsx index c75c2d7e7a2..932c939f607 100644 --- a/web/pages/[workspaceSlug]/settings/imports.tsx +++ b/web/pages/[workspaceSlug]/settings/imports.tsx @@ -10,7 +10,7 @@ import { NextPageWithLayout } from "types/app"; const ImportsPage: NextPageWithLayout = () => (
    -
    +

    Imports

    diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index 90c9848ca63..303c0a9d1da 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -1,7 +1,9 @@ import { useState, ReactElement } from "react"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; // hooks -import useUser from "hooks/use-user"; +import useToast from "hooks/use-toast"; +import { useMobxStore } from "lib/mobx/store-provider"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; @@ -14,15 +16,41 @@ import { Button } from "@plane/ui"; import { Search } from "lucide-react"; // types import { NextPageWithLayout } from "types/app"; +import { IWorkspaceBulkInviteFormData } from "types"; -const WorkspaceMembersSettingsPage: NextPageWithLayout = () => { +const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; + // store + const { + workspaceMember: { inviteMembersToWorkspace }, + } = useMobxStore(); // states const [inviteModal, setInviteModal] = useState(false); const [searchQuery, setSearchQuery] = useState(""); // hooks - const { user } = useUser(); + const { setToastAlert } = useToast(); + + const handleWorkspaceInvite = (data: IWorkspaceBulkInviteFormData) => { + if (!workspaceSlug) return; + + return inviteMembersToWorkspace(workspaceSlug.toString(), data) + .then(async () => { + setInviteModal(false); + setToastAlert({ + type: "success", + title: "Success!", + message: "Invitations sent successfully.", + }); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: `${err.error ?? "Something went wrong. Please try again."}`, + }) + ); + }; return ( <> @@ -30,12 +58,11 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = () => { setInviteModal(false)} - workspaceSlug={workspaceSlug.toString()} - user={user} + onSubmit={handleWorkspaceInvite} /> )}
    -
    +

    Members

    @@ -55,7 +82,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = () => {
    ); -}; +}); WorkspaceMembersSettingsPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index 7a84cfd9ff4..42586283917 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -12,9 +12,7 @@ import Script from "next/script"; class MyDocument extends Document { render() { - const isSessionRecorderEnabled = parseInt( - process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0" - ); + const isSessionRecorderEnabled = parseInt(process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0"); return ( diff --git a/web/pages/api/slack-redirect.ts b/web/pages/api/slack-redirect.ts deleted file mode 100644 index a6b8dbf4b79..00000000000 --- a/web/pages/api/slack-redirect.ts +++ /dev/null @@ -1,23 +0,0 @@ -import axios from "axios"; -import { NextApiRequest, NextApiResponse } from "next"; - -export default async function handleSlackAuthorize(req: NextApiRequest, res: NextApiResponse) { - try { - const { code } = req.body; - - if (!code || code === "") return res.status(400).json({ message: "Code is required" }); - - const response = await axios({ - method: "post", - url: process.env.SLACK_OAUTH_URL || "", - params: { - client_id: process.env.SLACK_CLIENT_ID, - client_secret: process.env.SLACK_CLIENT_SECRET, - code, - }, - }); - res.status(200).json(response?.data); - } catch (error) { - res.status(200).json({ message: "Internal Server Error" }); - } -} diff --git a/web/pages/create-workspace.tsx b/web/pages/create-workspace.tsx index 51ae06b9e22..7d3a45610bb 100644 --- a/web/pages/create-workspace.tsx +++ b/web/pages/create-workspace.tsx @@ -18,23 +18,23 @@ import { IWorkspace } from "types"; import { NextPageWithLayout } from "types/app"; const CreateWorkspacePage: NextPageWithLayout = observer(() => { + // router + const router = useRouter(); + // store + const { + user: { currentUser, updateCurrentUser }, + } = useMobxStore(); + // states const [defaultValues, setDefaultValues] = useState({ name: "", slug: "", organization_size: "", }); - - const router = useRouter(); - - const { user: userStore } = useMobxStore(); - const user = userStore.currentUser; - + // hooks const { theme } = useTheme(); const onSubmit = async (workspace: IWorkspace) => { - await userStore - .updateCurrentUser({ last_workspace_id: workspace.id }) - .then(() => router.push(`/${workspace.slug}`)); + await updateCurrentUser({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`)); }; return ( @@ -54,7 +54,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
    - {user?.email} + {currentUser?.email}
    diff --git a/web/pages/installations/[provider]/index.tsx b/web/pages/installations/[provider]/index.tsx index ac8a2fc22ac..243065a7c60 100644 --- a/web/pages/installations/[provider]/index.tsx +++ b/web/pages/installations/[provider]/index.tsx @@ -12,7 +12,7 @@ const appInstallationService = new AppInstallationService(); const AppPostInstallation: NextPageWithLayout = () => { const router = useRouter(); - const { installation_id, setup_action, state, provider, code } = router.query; + const { installation_id, state, provider, code } = router.query; useEffect(() => { if (provider === "github" && state && installation_id) { @@ -27,53 +27,37 @@ const AppPostInstallation: NextPageWithLayout = () => { console.log(err); }); } else if (provider === "slack" && state && code) { - appInstallationService - .getSlackAuthDetails(code.toString()) - .then((res) => { - const [workspaceSlug, projectId, integrationId] = state.toString().split(","); - - if (!projectId) { - const payload = { - metadata: { - ...res, - }, - }; + const [workspaceSlug, projectId, integrationId] = state.toString().split(","); - appInstallationService - .addInstallationApp(state.toString(), provider, payload) - .then((r) => { - window.opener = null; - window.open("", "_self"); - window.close(); - }) - .catch((err) => { - throw err?.response; - }); - } else { - const payload = { - access_token: res.access_token, - bot_user_id: res.bot_user_id, - webhook_url: res.incoming_webhook.url, - data: res, - team_id: res.team.id, - team_name: res.team.name, - scopes: res.scope, - }; - appInstallationService - .addSlackChannel(workspaceSlug, projectId, integrationId, payload) - .then((r) => { - window.opener = null; - window.open("", "_self"); - window.close(); - }) - .catch((err) => { - throw err.response; - }); - } - }) - .catch((err) => { - console.log(err); - }); + if (!projectId) { + const payload = { + code, + }; + appInstallationService + .addInstallationApp(state.toString(), provider, payload) + .then(() => { + window.opener = null; + window.open("", "_self"); + window.close(); + }) + .catch((err) => { + throw err?.response; + }); + } else { + const payload = { + code, + }; + appInstallationService + .addSlackChannel(workspaceSlug, projectId, integrationId, payload) + .then(() => { + window.opener = null; + window.open("", "_self"); + window.close(); + }) + .catch((err) => { + throw err.response; + }); + } } }, [state, installation_id, provider, code]); diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index f8c5e3e6fe1..cd64290c785 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -30,9 +30,12 @@ const workspaceService = new WorkspaceService(); const OnboardingPage: NextPageWithLayout = observer(() => { const [step, setStep] = useState(null); - const { user: userStore, workspace: workspaceStore } = useMobxStore(); + const { + user: { currentUser, updateCurrentUser, updateUserOnBoard }, + workspace: workspaceStore, + } = useMobxStore(); - const user = userStore.currentUser ?? undefined; + const user = currentUser ?? undefined; const workspaces = workspaceStore.workspaces; const userWorkspaces = workspaceStore.workspacesCreateByCurrentUser; @@ -48,7 +51,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => { const updateLastWorkspace = async () => { if (!workspaces) return; - await userStore.updateCurrentUser({ + await updateCurrentUser({ last_workspace_id: workspaces[0]?.id, }); }; @@ -64,14 +67,14 @@ const OnboardingPage: NextPageWithLayout = observer(() => { }, }; - await userStore.updateCurrentUser(payload); + await updateCurrentUser(payload); }; // complete onboarding const finishOnboarding = async () => { if (!user) return; - await userStore.updateUserOnBoard(); + await updateUserOnBoard(); }; useEffect(() => { diff --git a/web/public/services/jira.png b/web/public/services/jira.png deleted file mode 100644 index ca9db1926a6..00000000000 Binary files a/web/public/services/jira.png and /dev/null differ diff --git a/web/public/services/jira.svg b/web/public/services/jira.svg new file mode 100644 index 00000000000..5e5cebc843a --- /dev/null +++ b/web/public/services/jira.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/web/services/app_config.service.ts b/web/services/app_config.service.ts index 5843c01c96e..8f8bcd42381 100644 --- a/web/services/app_config.service.ts +++ b/web/services/app_config.service.ts @@ -2,27 +2,21 @@ import { APIService } from "services/api.service"; // helper import { API_BASE_URL } from "helpers/common.helper"; - -export interface IEnvConfig { - github: string; - google: string; - github_app_name: string | null; - email_password_login: boolean; - magic_login: boolean; -} +// types +import { IAppConfig } from "types/app"; export class AppConfigService extends APIService { constructor() { super(API_BASE_URL); } - async envConfig(): Promise { + async envConfig(): Promise { return this.get("/api/configs/", { headers: { "Content-Type": "application/json", }, }) - .then((response) => response?.data) + .then((response) => response.data) .catch((error) => { throw error?.response?.data; }); diff --git a/web/services/app_installation.service.ts b/web/services/app_installation.service.ts index 2a7a4ea6af4..17972103640 100644 --- a/web/services/app_installation.service.ts +++ b/web/services/app_installation.service.ts @@ -60,16 +60,4 @@ export class AppInstallationService extends APIService { throw error?.response; }); } - - async getSlackAuthDetails(code: string): Promise { - const response = await this.request({ - method: "post", - url: "/api/slack-redirect", - data: { - code, - }, - }); - - return response.data; - } } diff --git a/web/services/cycle.service.ts b/web/services/cycle.service.ts index 65188515ccd..2e8b0b9981b 100644 --- a/web/services/cycle.service.ts +++ b/web/services/cycle.service.ts @@ -71,23 +71,6 @@ export class CycleService extends APIService { }); } - async updateCycle( - workspaceSlug: string, - projectId: string, - cycleId: string, - data: any, - user: IUser | undefined - ): Promise { - return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, data) - .then((response) => { - trackEventService.trackCycleEvent(response?.data, "CYCLE_UPDATE", user as IUser); - return response?.data; - }) - .catch((error) => { - throw error?.response?.data; - }); - } - async patchCycle( workspaceSlug: string, projectId: string, @@ -97,7 +80,7 @@ export class CycleService extends APIService { ): Promise { return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, data) .then((response) => { - trackEventService.trackCycleEvent(response?.data, "CYCLE_UPDATE", user as IUser); + if (user) trackEventService.trackCycleEvent(response?.data, "CYCLE_UPDATE", user); return response?.data; }) .catch((error) => { diff --git a/web/services/file.service.ts b/web/services/file.service.ts index 0e3749a4cd8..84907161e3b 100644 --- a/web/services/file.service.ts +++ b/web/services/file.service.ts @@ -2,6 +2,7 @@ import { APIService } from "services/api.service"; // helpers import { API_BASE_URL } from "helpers/common.helper"; +import axios from "axios"; export interface UnSplashImage { id: string; @@ -28,25 +29,38 @@ export interface UnSplashImageUrls { } export class FileService extends APIService { + private cancelSource: any; + constructor() { super(API_BASE_URL); this.uploadFile = this.uploadFile.bind(this); this.deleteImage = this.deleteImage.bind(this); + this.cancelUpload = this.cancelUpload.bind(this); } async uploadFile(workspaceSlug: string, file: FormData): Promise { + this.cancelSource = axios.CancelToken.source(); return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, { headers: { ...this.getHeaders(), "Content-Type": "multipart/form-data", }, + cancelToken: this.cancelSource.token, }) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + if (axios.isCancel(error)) { + console.log(error.message); + } else { + throw error?.response?.data; + } }); } + cancelUpload() { + this.cancelSource.cancel("Upload cancelled"); + } + getUploadFileFunction(workspaceSlug: string): (file: File) => Promise { return async (file: File) => { const formData = new FormData(); diff --git a/web/services/project/index.ts b/web/services/project/index.ts index 538b65e1182..18cf1200aa3 100644 --- a/web/services/project/index.ts +++ b/web/services/project/index.ts @@ -1,6 +1,6 @@ export * from "./project.service"; -export * from "./project_estimate.service"; -export * from "./project_publish.service"; -export * from "./project_state.service"; -export * from "./project_export.service"; -export * from "./project_invitation.service"; +export * from "./project-estimate.service"; +export * from "./project-export.service"; +export * from "./project-member.service"; +export * from "./project-state.service"; +export * from "./project-publish.service"; diff --git a/web/services/project/project_estimate.service.ts b/web/services/project/project-estimate.service.ts similarity index 100% rename from web/services/project/project_estimate.service.ts rename to web/services/project/project-estimate.service.ts diff --git a/web/services/project/project_export.service.ts b/web/services/project/project-export.service.ts similarity index 100% rename from web/services/project/project_export.service.ts rename to web/services/project/project-export.service.ts diff --git a/web/services/project/project-member.service.ts b/web/services/project/project-member.service.ts new file mode 100644 index 00000000000..35c7a620aab --- /dev/null +++ b/web/services/project/project-member.service.ts @@ -0,0 +1,109 @@ +import { API_BASE_URL } from "helpers/common.helper"; +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import type { IUser, IProjectBulkAddFormData, IProjectMember, IProjectMemberInvitation } from "types"; + +const trackEventService = new TrackEventService(); + +export class ProjectMemberService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async fetchProjectMembers(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async bulkAddMembersToProject( + workspaceSlug: string, + projectId: string, + data: IProjectBulkAddFormData, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`, data) + .then((response) => { + trackEventService.trackProjectEvent( + { + workspaceId: response?.data?.workspace?.id, + workspaceSlug, + projectId, + projectName: response?.data?.project?.name, + memberEmail: response?.data?.member?.email, + }, + "PROJECT_MEMBER_INVITE", + user as IUser + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async projectMemberMe(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async getProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateProjectMember( + workspaceSlug: string, + projectId: string, + memberId: string, + data: Partial + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async fetchProjectInvitations(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateProjectInvitation(workspaceSlug: string, projectId: string, invitationId: string): Promise { + return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/${invitationId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteProjectInvitation(workspaceSlug: string, projectId: string, invitationId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/${invitationId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/project/project_publish.service.ts b/web/services/project/project-publish.service.ts similarity index 100% rename from web/services/project/project_publish.service.ts rename to web/services/project/project-publish.service.ts diff --git a/web/services/project/project_state.service.ts b/web/services/project/project-state.service.ts similarity index 88% rename from web/services/project/project_state.service.ts rename to web/services/project/project-state.service.ts index 018df3a2053..44b649327a4 100644 --- a/web/services/project/project_state.service.ts +++ b/web/services/project/project-state.service.ts @@ -4,7 +4,7 @@ import { TrackEventService } from "services/track_event.service"; // helpers import { API_BASE_URL } from "helpers/common.helper"; // types -import type { IUser, IState, IStateResponse } from "types"; +import type { IUser, IState } from "types"; const trackEventService = new TrackEventService(); @@ -13,7 +13,7 @@ export class ProjectStateService extends APIService { super(API_BASE_URL); } - async createState(workspaceSlug: string, projectId: string, data: any, user: IUser | undefined): Promise { + async createState(workspaceSlug: string, projectId: string, data: any, user: IUser | undefined): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data) .then((response) => { trackEventService.trackStateEvent(response?.data, "STATE_CREATE", user as IUser); @@ -24,17 +24,16 @@ export class ProjectStateService extends APIService { }); } - async getStates(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`) + async markDefault(workspaceSlug: string, projectId: string, stateId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/mark-default/`, {}) .then((response) => response?.data) .catch((error) => { - throw error?.response?.data; + throw error?.response; }); } - async getIssuesByState(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/?group_by=state`) - + async getStates(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/services/project/project.service.ts b/web/services/project/project.service.ts index 1c824a40c10..74d678d23c8 100644 --- a/web/services/project/project.service.ts +++ b/web/services/project/project.service.ts @@ -5,10 +5,7 @@ import { TrackEventService } from "services/track_event.service"; // types import type { GithubRepositoriesResponse, - IUser, IProject, - IProjectBulkAddFormData, - IProjectMember, ISearchIssueResponse, ProjectPreferences, IProjectViewProps, @@ -110,77 +107,6 @@ export class ProjectService extends APIService { }); } - async fetchProjectMembers(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async bulkAddMembersToProject( - workspaceSlug: string, - projectId: string, - data: IProjectBulkAddFormData, - user: IUser | undefined - ): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`, data) - .then((response) => { - trackEventService.trackProjectEvent( - { - workspaceId: response?.data?.workspace?.id, - workspaceSlug, - projectId, - projectName: response?.data?.project?.name, - memberEmail: response?.data?.member?.email, - }, - "PROJECT_MEMBER_INVITE", - user as IUser - ); - return response?.data; - }) - .catch((error) => { - throw error?.response?.data; - }); - } - - async projectMemberMe(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); - } - - async getProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async updateProjectMember( - workspaceSlug: string, - projectId: string, - memberId: string, - data: Partial - ): Promise { - return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async deleteProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - async setProjectView( workspaceSlug: string, projectId: string, diff --git a/web/services/project/project_invitation.service.ts b/web/services/project/project_invitation.service.ts deleted file mode 100644 index 1fbf6e24c24..00000000000 --- a/web/services/project/project_invitation.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { API_BASE_URL } from "helpers/common.helper"; -// services -import { APIService } from "services/api.service"; -// types -import { IProjectMemberInvitation } from "types"; - -export class ProjectInvitationService extends APIService { - constructor() { - super(API_BASE_URL); - } - - async fetchProjectInvitations(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async updateProjectInvitation(workspaceSlug: string, projectId: string, invitationId: string): Promise { - return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/${invitationId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async deleteProjectInvitation(workspaceSlug: string, projectId: string, invitationId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/${invitationId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } -} diff --git a/web/store/app-config.store.ts b/web/store/app-config.store.ts new file mode 100644 index 00000000000..3a4d9efc0a5 --- /dev/null +++ b/web/store/app-config.store.ts @@ -0,0 +1,47 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "./root"; +import { IAppConfig } from "types/app"; +// services +import { AppConfigService } from "services/app_config.service"; + +export interface IAppConfigStore { + envConfig: IAppConfig | null; + // action + fetchAppConfig: () => Promise; +} + +class AppConfigStore implements IAppConfigStore { + // observables + envConfig: IAppConfig | null = null; + + // root store + rootStore; + // service + appConfigService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + envConfig: observable.ref, + // actions + fetchAppConfig: action, + }); + this.appConfigService = new AppConfigService(); + + this.rootStore = _rootStore; + } + fetchAppConfig = async () => { + try { + const config = await this.appConfigService.envConfig(); + runInAction(() => { + this.envConfig = config; + }); + return config; + } catch (error) { + throw error; + } + }; +} + +export default AppConfigStore; diff --git a/web/store/archived-issues/issue_filters.store.ts b/web/store/archived-issues/issue_filters.store.ts index edb1549a630..9a555212b15 100644 --- a/web/store/archived-issues/issue_filters.store.ts +++ b/web/store/archived-issues/issue_filters.store.ts @@ -3,7 +3,7 @@ import { observable, computed, makeObservable, action, runInAction } from "mobx" import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; // services import { IssueService } from "services/issue"; -import { ProjectService } from "services/project"; +import { ProjectService, ProjectMemberService } from "services/project"; // types import { RootStore } from "../root"; import { @@ -25,6 +25,7 @@ export interface IArchivedIssueFilterStore { // services projectService: ProjectService; + projectMemberService: ProjectMemberService; issueService: IssueService; // computed @@ -87,6 +88,7 @@ export class ArchivedIssueFilterStore implements IArchivedIssueFilterStore { // services projectService: ProjectService; + projectMemberService: ProjectMemberService; issueService: IssueService; constructor(_rootStore: RootStore) { @@ -111,6 +113,7 @@ export class ArchivedIssueFilterStore implements IArchivedIssueFilterStore { // services this.issueService = new IssueService(); this.projectService = new ProjectService(); + this.projectMemberService = new ProjectMemberService(); } computedFilter = (filters: any, filteredParams: any) => { @@ -222,7 +225,7 @@ export class ArchivedIssueFilterStore implements IArchivedIssueFilterStore { fetchUserProjectFilters = async (workspaceSlug: string, projectId: string) => { try { - const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId); + const memberResponse = await this.projectMemberService.projectMemberMe(workspaceSlug, projectId); const issueProperties = await this.issueService.getIssueDisplayProperties(workspaceSlug, projectId); runInAction(() => { diff --git a/web/store/cycle/cycle_issue.store.ts b/web/store/cycle/cycle_issue.store.ts index 333218060f4..17d8f351cbf 100644 --- a/web/store/cycle/cycle_issue.store.ts +++ b/web/store/cycle/cycle_issue.store.ts @@ -36,7 +36,7 @@ export interface ICycleIssueStore { updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; updateGanttIssueStructure: (workspaceSlug: string, cycleId: string, issue: IIssue, payload: IBlockUpdateData) => void; deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; - addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => void; + addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) => void; } @@ -322,7 +322,7 @@ export class CycleIssueStore implements ICycleIssueStore { } }; - addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { + addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { const user = this.rootStore.user.currentUser ?? undefined; @@ -331,7 +331,7 @@ export class CycleIssueStore implements ICycleIssueStore { projectId, cycleId, { - issues: [issueId], + issues: issueIds, }, user ); diff --git a/web/store/cycle/cycle_issue_calendar_view.store.ts b/web/store/cycle/cycle_issue_calendar_view.store.ts index fa80f39ac24..3b695d69778 100644 --- a/web/store/cycle/cycle_issue_calendar_view.store.ts +++ b/web/store/cycle/cycle_issue_calendar_view.store.ts @@ -36,8 +36,8 @@ export class CycleIssueCalendarViewStore implements ICycleIssueCalendarViewStore projectId: projectId, }; - const droppableSourceColumnId = source.droppableId; - const droppableDestinationColumnId = destination.droppableId; + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; if (droppableSourceColumnId === droppableDestinationColumnId) return; diff --git a/web/store/cycle/cycle_issue_filters.store.ts b/web/store/cycle/cycle_issue_filters.store.ts index c6818b6df21..95fad6d9856 100644 --- a/web/store/cycle/cycle_issue_filters.store.ts +++ b/web/store/cycle/cycle_issue_filters.store.ts @@ -133,7 +133,9 @@ export class CycleIssueFilterStore implements ICycleIssueFilterStore { }, }; - await this.cycleService.updateCycle(workspaceSlug, projectId, cycleId, payload, undefined); + const user = this.rootStore.user.currentUser ?? undefined; + + await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, payload, user); } catch (error) { this.fetchCycleFilters(workspaceSlug, projectId, cycleId); diff --git a/web/store/cycle/cycle_issue_kanban_view.store.ts b/web/store/cycle/cycle_issue_kanban_view.store.ts index b007e11ad23..0ecc96e60ad 100644 --- a/web/store/cycle/cycle_issue_kanban_view.store.ts +++ b/web/store/cycle/cycle_issue_kanban_view.store.ts @@ -95,9 +95,9 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; @@ -315,9 +315,9 @@ export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; diff --git a/web/store/cycle/cycles.store.ts b/web/store/cycle/cycles.store.ts index 06ac6185465..67e3fc2ca1d 100644 --- a/web/store/cycle/cycles.store.ts +++ b/web/store/cycle/cycles.store.ts @@ -7,6 +7,7 @@ import { RootStore } from "../root"; import { ProjectService } from "services/project"; import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; +import { getDateRangeStatus } from "helpers/date-time.helper"; export interface ICycleStore { loader: boolean; @@ -17,22 +18,28 @@ export interface ICycleStore { cycleId: string | null; cycles: { - [project_id: string]: ICycle[]; + [projectId: string]: { + [filterType: string]: ICycle[]; + }; }; cycle_details: { - [cycle_id: string]: ICycle; + [cycleId: string]: ICycle; }; active_cycle_issues: { - [cycle_id: string]: IIssue[]; + [cycleId: string]: IIssue[]; }; // computed getCycleById: (cycleId: string) => ICycle | null; + projectCycles: ICycle[] | null; + projectCompletedCycles: ICycle[] | null; + projectUpcomingCycles: ICycle[] | null; + projectDraftCycles: ICycle[] | null; // actions setCycleView: (_cycleView: TCycleView) => void; setCycleLayout: (_cycleLayout: TCycleLayout) => void; - setCycleId: (cycleId: string) => void; + setCycleId: (cycleId: string | null) => void; validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; @@ -44,12 +51,12 @@ export interface ICycleStore { fetchCycleWithId: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; fetchActiveCycleIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; - createCycle: (workspaceSlug: string, projectId: string, data: any) => Promise; - updateCycle: (workspaceSlug: string, projectId: string, cycleId: string, data: any) => Promise; + createCycle: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + patchCycle: (workspaceSlug: string, projectId: string, cycleId: string, data: Partial) => Promise; removeCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; - addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; - removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + addCycleToFavorites: (workspaceSlug: string, projectId: string, cycle: ICycle) => Promise; + removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycle: ICycle) => Promise; } export class CycleStore implements ICycleStore { @@ -61,15 +68,17 @@ export class CycleStore implements ICycleStore { cycleId: string | null = null; cycles: { - [project_id: string]: ICycle[]; + [projectId: string]: { + [filterType: string]: ICycle[]; + }; } = {}; cycle_details: { - [cycle_id: string]: ICycle; + [cycleId: string]: ICycle; } = {}; active_cycle_issues: { - [cycle_id: string]: IIssue[]; + [cycleId: string]: IIssue[]; } = {}; // root store @@ -94,6 +103,9 @@ export class CycleStore implements ICycleStore { // computed projectCycles: computed, + projectCompletedCycles: computed, + projectUpcomingCycles: computed, + projectDraftCycles: computed, // actions setCycleView: action, @@ -107,7 +119,6 @@ export class CycleStore implements ICycleStore { fetchActiveCycleIssues: action, createCycle: action, - updateCycle: action, removeCycle: action, addCycleToFavorites: action, @@ -122,8 +133,34 @@ export class CycleStore implements ICycleStore { // computed get projectCycles() { - if (!this.rootStore.project.projectId) return null; - return this.cycles[this.rootStore.project.projectId] || null; + const projectId = this.rootStore.project.projectId; + + if (!projectId) return null; + return this.cycles[projectId]?.all || null; + } + + get projectCompletedCycles() { + const projectId = this.rootStore.project.projectId; + + if (!projectId) return null; + + return this.cycles[projectId]?.completed || null; + } + + get projectUpcomingCycles() { + const projectId = this.rootStore.project.projectId; + + if (!projectId) return null; + + return this.cycles[projectId]?.upcoming || null; + } + + get projectDraftCycles() { + const projectId = this.rootStore.project.projectId; + + if (!projectId) return null; + + return this.cycles[projectId]?.draft || null; } getCycleById = (cycleId: string) => this.cycle_details[cycleId] || null; @@ -131,7 +168,7 @@ export class CycleStore implements ICycleStore { // actions setCycleView = (_cycleView: TCycleView) => (this.cycleView = _cycleView); setCycleLayout = (_cycleLayout: TCycleLayout) => (this.cycleLayout = _cycleLayout); - setCycleId = (cycleId: string) => (this.cycleId = cycleId); + setCycleId = (cycleId: string | null) => (this.cycleId = cycleId); validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => { try { @@ -159,7 +196,7 @@ export class CycleStore implements ICycleStore { runInAction(() => { this.cycles = { ...this.cycles, - [projectId]: cyclesResponse, + [projectId]: { ...this.cycles[projectId], [params]: cyclesResponse }, }; this.loader = false; this.error = null; @@ -209,16 +246,14 @@ export class CycleStore implements ICycleStore { } }; - createCycle = async (workspaceSlug: string, projectId: string, data: any) => { + createCycle = async (workspaceSlug: string, projectId: string, data: Partial) => { try { - console.log("Cycle Creating"); const response = await this.cycleService.createCycle( workspaceSlug, projectId, data, this.rootStore.user.currentUser ); - console.log("Cycle created"); runInAction(() => { this.cycle_details = { @@ -237,30 +272,7 @@ export class CycleStore implements ICycleStore { } }; - updateCycle = async (workspaceSlug: string, projectId: string, cycleId: string, data: any) => { - try { - const response = await this.cycleService.updateCycle(workspaceSlug, projectId, cycleId, data, undefined); - - const _cycleDetails = { - ...this.cycle_details, - [cycleId]: { ...this.cycle_details[cycleId], ...response }, - }; - - runInAction(() => { - this.cycle_details = _cycleDetails; - }); - - const _currentView = this.cycleView === "active" ? "current" : this.cycleView; - this.fetchCycles(workspaceSlug, projectId, _currentView); - - return response; - } catch (error) { - console.log("Failed to update cycle from cycle store"); - throw error; - } - }; - - patchCycle = async (workspaceSlug: string, projectId: string, cycleId: string, data: any) => { + patchCycle = async (workspaceSlug: string, projectId: string, cycleId: string, data: Partial) => { try { const _response = await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, data, undefined); @@ -297,30 +309,60 @@ export class CycleStore implements ICycleStore { } }; - addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { + addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycle: ICycle) => { + const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + + const statusCyclesList = this.cycles[projectId]?.[cycleStatus] ?? []; + const allCyclesList = this.projectCycles ?? []; + try { runInAction(() => { this.cycles = { ...this.cycles, - [projectId]: this.cycles[projectId].map((cycle) => { - if (cycle.id === cycleId) return { ...cycle, is_favorite: true }; - return cycle; - }), + [projectId]: { + ...this.cycles[projectId], + [cycleStatus]: statusCyclesList?.map((c) => { + if (c.id === cycle.id) return { ...c, is_favorite: true }; + return c; + }), + all: allCyclesList?.map((c) => { + if (c.id === cycle.id) return { ...c, is_favorite: true }; + return c; + }), + }, + }; + this.cycle_details = { + ...this.cycle_details, + [cycle.id]: { ...this.cycle_details[cycle.id], is_favorite: true }, }; }); + // updating through api. - const response = await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycleId }); + const response = await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycle.id }); + return response; } catch (error) { console.log("Failed to add cycle to favorites in the cycles store", error); - // resetting the local state + + // reset on error runInAction(() => { this.cycles = { ...this.cycles, - [projectId]: this.cycles[projectId].map((cycle) => { - if (cycle.id === cycleId) return { ...cycle, is_favorite: false }; - return cycle; - }), + [projectId]: { + ...this.cycles[projectId], + [cycleStatus]: statusCyclesList?.map((c) => { + if (c.id === cycle.id) return { ...c, is_favorite: false }; + return c; + }), + all: allCyclesList?.map((c) => { + if (c.id === cycle.id) return { ...c, is_favorite: false }; + return c; + }), + }, + }; + this.cycle_details = { + ...this.cycle_details, + [cycle.id]: { ...this.cycle_details[cycle.id], is_favorite: false }, }; }); @@ -328,30 +370,62 @@ export class CycleStore implements ICycleStore { } }; - removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { + removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycle: ICycle) => { + const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + + const statusCyclesList = this.cycles[projectId]?.[cycleStatus] ?? []; + const allCyclesList = this.projectCycles ?? []; + try { runInAction(() => { this.cycles = { ...this.cycles, - [projectId]: this.cycles[projectId].map((cycle) => { - if (cycle.id === cycleId) return { ...cycle, is_favorite: false }; - return cycle; - }), + [projectId]: { + ...this.cycles[projectId], + [cycleStatus]: statusCyclesList?.map((c) => { + if (c.id === cycle.id) return { ...c, is_favorite: false }; + return c; + }), + all: allCyclesList?.map((c) => { + if (c.id === cycle.id) return { ...c, is_favorite: false }; + return c; + }), + }, + }; + this.cycle_details = { + ...this.cycle_details, + [cycle.id]: { ...this.cycle_details[cycle.id], is_favorite: false }, }; }); - const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId); + + const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycle.id); + return response; } catch (error) { console.log("Failed to remove cycle from favorites - Cycle Store", error); + + // reset on error runInAction(() => { this.cycles = { ...this.cycles, - [projectId]: this.cycles[projectId].map((cycle) => { - if (cycle.id === cycleId) return { ...cycle, is_favorite: true }; - return cycle; - }), + [projectId]: { + ...this.cycles[projectId], + [cycleStatus]: statusCyclesList?.map((c) => { + if (c.id === cycle.id) return { ...c, is_favorite: true }; + return c; + }), + all: allCyclesList?.map((c) => { + if (c.id === cycle.id) return { ...c, is_favorite: true }; + return c; + }), + }, + }; + this.cycle_details = { + ...this.cycle_details, + [cycle.id]: { ...this.cycle_details[cycle.id], is_favorite: true }, }; }); + throw error; } }; diff --git a/web/store/editor/index.ts b/web/store/editor/index.ts index ff3ce7a3349..f1e23e8135b 100644 --- a/web/store/editor/index.ts +++ b/web/store/editor/index.ts @@ -1 +1 @@ -export * from "./mentions.store" \ No newline at end of file +export * from "./mentions.store"; diff --git a/web/store/editor/mentions.store.ts b/web/store/editor/mentions.store.ts index 4bf1f45c3af..df4ba152e3a 100644 --- a/web/store/editor/mentions.store.ts +++ b/web/store/editor/mentions.store.ts @@ -3,43 +3,44 @@ import { RootStore } from "../root"; import { computed, makeObservable } from "mobx"; export interface IMentionsStore { - mentionSuggestions: IMentionSuggestion[]; - mentionHighlights: IMentionHighlight[]; + mentionSuggestions: IMentionSuggestion[]; + mentionHighlights: IMentionHighlight[]; } -export class MentionsStore implements IMentionsStore{ +export class MentionsStore implements IMentionsStore { + // root store + rootStore; - // root store - rootStore; + constructor(_rootStore: RootStore) { + // rootStore + this.rootStore = _rootStore; - constructor(_rootStore: RootStore ){ + makeObservable(this, { + mentionHighlights: computed, + mentionSuggestions: computed, + }); + } - // rootStore - this.rootStore = _rootStore; + get mentionSuggestions() { + const projectMembers = this.rootStore.projectMember.projectMembers; - makeObservable(this, { - mentionHighlights: computed, - mentionSuggestions: computed - }) - } - - get mentionSuggestions() { - const projectMembers = this.rootStore.project.projectMembers - - const suggestions = projectMembers === null ? [] : projectMembers.map((member) => ({ + const suggestions = + projectMembers === null + ? [] + : projectMembers.map((member) => ({ id: member.member.id, type: "User", title: member.member.display_name, subtitle: member.member.email ?? "", avatar: member.member.avatar, redirect_uri: `/${member.workspace.slug}/profile/${member.member.id}`, - })) + })); - return suggestions - } + return suggestions; + } - get mentionHighlights() { - const user = this.rootStore.user.currentUser; - return user ? [user.id] : [] - } -} \ No newline at end of file + get mentionHighlights() { + const user = this.rootStore.user.currentUser; + return user ? [user.id] : []; + } +} diff --git a/web/store/global-view/global_views.store.ts b/web/store/global-view/global_views.store.ts index c9915b8d898..588f0a4d2cf 100644 --- a/web/store/global-view/global_views.store.ts +++ b/web/store/global-view/global_views.store.ts @@ -19,7 +19,7 @@ export interface IGlobalViewsStore { }; // actions - setGlobalViewId: (viewId: string) => void; + setGlobalViewId: (viewId: string | null) => void; fetchAllGlobalViews: (workspaceSlug: string) => Promise; fetchGlobalViewDetails: (workspaceSlug: string, viewId: string) => Promise; @@ -72,7 +72,7 @@ export class GlobalViewsStore implements IGlobalViewsStore { this.workspaceService = new WorkspaceService(); } - setGlobalViewId = (viewId: string) => { + setGlobalViewId = (viewId: string | null) => { this.globalViewId = viewId; }; diff --git a/web/store/inbox/inbox.store.ts b/web/store/inbox/inbox.store.ts index c1ca086098e..b29d3685597 100644 --- a/web/store/inbox/inbox.store.ts +++ b/web/store/inbox/inbox.store.ts @@ -22,7 +22,7 @@ export interface IInboxStore { }; // actions - setInboxId: (inboxId: string) => void; + setInboxId: (inboxId: string | null) => void; getInboxId: (projectId: string) => string | null; @@ -100,7 +100,7 @@ export class InboxStore implements IInboxStore { return this.inboxesList[projectId]?.[0]?.id ?? null; }; - setInboxId = (inboxId: string) => { + setInboxId = (inboxId: string | null) => { runInAction(() => { this.inboxId = inboxId; }); diff --git a/web/store/issue/issue_calendar_view.store.ts b/web/store/issue/issue_calendar_view.store.ts index 5f9cc89bf1c..881e2ee836d 100644 --- a/web/store/issue/issue_calendar_view.store.ts +++ b/web/store/issue/issue_calendar_view.store.ts @@ -35,8 +35,8 @@ export class IssueCalendarViewStore implements IIssueCalendarViewStore { projectId: projectId, }; - const droppableSourceColumnId = source.droppableId; - const droppableDestinationColumnId = destination.droppableId; + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; if (droppableSourceColumnId === droppableDestinationColumnId) return; diff --git a/web/store/issue/issue_filters.store.ts b/web/store/issue/issue_filters.store.ts index be28cca87d1..83b7f7bada0 100644 --- a/web/store/issue/issue_filters.store.ts +++ b/web/store/issue/issue_filters.store.ts @@ -1,6 +1,6 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"; // services -import { ProjectService } from "services/project"; +import { ProjectService, ProjectMemberService } from "services/project"; import { IssueService } from "services/issue"; // helpers import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; @@ -72,6 +72,7 @@ export class IssueFilterStore implements IIssueFilterStore { // services projectService; + projectMemberService; issueService; constructor(_rootStore: RootStore) { @@ -98,6 +99,7 @@ export class IssueFilterStore implements IIssueFilterStore { this.rootStore = _rootStore; this.projectService = new ProjectService(); + this.projectMemberService = new ProjectMemberService(); this.issueService = new IssueService(); } @@ -145,7 +147,7 @@ export class IssueFilterStore implements IIssueFilterStore { fetchUserProjectFilters = async (workspaceSlug: string, projectId: string) => { try { - const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId); + const memberResponse = await this.projectMemberService.projectMemberMe(workspaceSlug, projectId); const issueProperties = await this.issueService.getIssueDisplayProperties(workspaceSlug, projectId); runInAction(() => { diff --git a/web/store/issue/issue_kanban_view.store.ts b/web/store/issue/issue_kanban_view.store.ts index 25d217b7fb6..827972694ea 100644 --- a/web/store/issue/issue_kanban_view.store.ts +++ b/web/store/issue/issue_kanban_view.store.ts @@ -95,9 +95,9 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; @@ -315,9 +315,9 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; diff --git a/web/store/module/module_issue.store.ts b/web/store/module/module_issue.store.ts index 5b5893b0d3f..165b51b622d 100644 --- a/web/store/module/module_issue.store.ts +++ b/web/store/module/module_issue.store.ts @@ -40,7 +40,7 @@ export interface IModuleIssueStore { payload: IBlockUpdateData ) => void; deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; - addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise; + addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise; removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, bridgeId: string) => Promise; } @@ -337,7 +337,7 @@ export class ModuleIssueStore implements IModuleIssueStore { } }; - addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => { + addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => { try { const user = this.rootStore.user.currentUser ?? undefined; @@ -346,7 +346,7 @@ export class ModuleIssueStore implements IModuleIssueStore { projectId, moduleId, { - issues: [issueId], + issues: issueIds, }, user ); diff --git a/web/store/module/module_issue_calendar_view.store.ts b/web/store/module/module_issue_calendar_view.store.ts index 313745a18d7..3bfed3140a9 100644 --- a/web/store/module/module_issue_calendar_view.store.ts +++ b/web/store/module/module_issue_calendar_view.store.ts @@ -36,8 +36,8 @@ export class ModuleIssueCalendarViewStore implements IModuleIssueCalendarViewSto projectId: projectId, }; - const droppableSourceColumnId = source.droppableId; - const droppableDestinationColumnId = destination.droppableId; + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; if (droppableSourceColumnId === droppableDestinationColumnId) return; diff --git a/web/store/module/module_issue_kanban_view.store.ts b/web/store/module/module_issue_kanban_view.store.ts index a8584a7ff2a..82e210f29ff 100644 --- a/web/store/module/module_issue_kanban_view.store.ts +++ b/web/store/module/module_issue_kanban_view.store.ts @@ -95,9 +95,9 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; @@ -315,9 +315,9 @@ export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore { }; // source, destination group and sub group id - let droppableSourceColumnId = source.droppableId; + let droppableSourceColumnId = source?.droppableId || null; droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; - let droppableDestinationColumnId = destination.droppableId; + let droppableDestinationColumnId = destination?.droppableId || null; droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; diff --git a/web/store/module/modules.store.ts b/web/store/module/modules.store.ts index 91a11cd76d5..0dc122438bc 100644 --- a/web/store/module/modules.store.ts +++ b/web/store/module/modules.store.ts @@ -34,7 +34,7 @@ export interface IModuleStore { }; // actions - setModuleId: (moduleSlug: string) => void; + setModuleId: (moduleId: string | null) => void; getModuleById: (moduleId: string) => IModule | null; @@ -144,8 +144,8 @@ export class ModuleStore implements IModuleStore { getModuleById = (moduleId: string) => this.moduleDetails[moduleId] || null; // actions - setModuleId = (moduleSlug: string) => { - this.moduleId = moduleSlug ?? null; + setModuleId = (moduleId: string | null) => { + this.moduleId = moduleId; }; fetchModules = async (workspaceSlug: string, projectId: string) => { diff --git a/web/store/project-view/project_view_issue_calendar_view.store.ts b/web/store/project-view/project_view_issue_calendar_view.store.ts index 2f70df136d6..9bce218aee8 100644 --- a/web/store/project-view/project_view_issue_calendar_view.store.ts +++ b/web/store/project-view/project_view_issue_calendar_view.store.ts @@ -36,8 +36,8 @@ export class ProjectViewIssueCalendarViewStore implements IProjectViewIssueCalen projectId: projectId, }; - const droppableSourceColumnId = source.droppableId; - const droppableDestinationColumnId = destination.droppableId; + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; if (droppableSourceColumnId === droppableDestinationColumnId) return; diff --git a/web/store/project-view/project_views.store.ts b/web/store/project-view/project_views.store.ts index 4c4baf487cc..76c58002dfd 100644 --- a/web/store/project-view/project_views.store.ts +++ b/web/store/project-view/project_views.store.ts @@ -20,7 +20,7 @@ export interface IProjectViewsStore { }; // actions - setViewId: (viewId: string) => void; + setViewId: (viewId: string | null) => void; fetchAllViews: (workspaceSlug: string, projectId: string) => Promise; fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise; @@ -82,7 +82,7 @@ export class ProjectViewsStore implements IProjectViewsStore { this.viewService = new ViewService(); } - setViewId = (viewId: string) => { + setViewId = (viewId: string | null) => { this.viewId = viewId; }; diff --git a/web/store/project/index.ts b/web/store/project/index.ts index ccd0abfaf2e..35cfda8fce5 100644 --- a/web/store/project/index.ts +++ b/web/store/project/index.ts @@ -1,5 +1,6 @@ -export * from "./project_publish.store"; export * from "./project.store"; -export * from "./project_estimates.store"; -export * from "./project_label_store"; -export * from "./project_state.store"; +export * from "./project-estimates.store"; +export * from "./project-label.store"; +export * from "./project-members.store"; +export * from "./project-publish.store"; +export * from "./project-state.store"; diff --git a/web/store/project/project_estimates.store.ts b/web/store/project/project-estimates.store.ts similarity index 100% rename from web/store/project/project_estimates.store.ts rename to web/store/project/project-estimates.store.ts diff --git a/web/store/project/project_label_store.ts b/web/store/project/project-label.store.ts similarity index 100% rename from web/store/project/project_label_store.ts rename to web/store/project/project-label.store.ts diff --git a/web/store/project/project-members.store.ts b/web/store/project/project-members.store.ts new file mode 100644 index 00000000000..d75bbeec61f --- /dev/null +++ b/web/store/project/project-members.store.ts @@ -0,0 +1,182 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IProjectMember } from "types"; +// services +import { ProjectMemberService } from "services/project"; + +export interface IProjectMemberStore { + // observables + members: { + [projectId: string]: IProjectMember[] | null; // project_id: members + }; + // computed + projectMembers: IProjectMember[] | null; + // actions + getProjectMemberById: (memberId: string) => IProjectMember | null; + getProjectMemberByUserId: (memberId: string) => IProjectMember | null; + fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise; + removeMemberFromProject: (workspaceSlug: string, projectId: string, memberId: string) => Promise; + updateMember: ( + workspaceSlug: string, + projectId: string, + memberId: string, + data: Partial + ) => Promise; + + deleteProjectInvitation: (workspaceSlug: string, projectId: string, memberId: string) => Promise; +} + +export class ProjectMemberStore implements IProjectMemberStore { + members: { + [projectId: string]: IProjectMember[]; // projectId: members + } = {}; + + // root store + rootStore; + // service + projectMemberService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + members: observable.ref, + // computed + projectMembers: computed, + // action + getProjectMemberById: action, + fetchProjectMembers: action, + removeMemberFromProject: action, + updateMember: action, + }); + + this.rootStore = _rootStore; + this.projectMemberService = new ProjectMemberService(); + } + + /** + * Computed value of current members in the project + */ + get projectMembers() { + if (!this.rootStore.project.projectId) return null; + return this.members[this.rootStore.project.projectId] || null; + } + + /** + * Get all project information using membership id + * @param memberId + * @returns + */ + getProjectMemberById = (memberId: string) => { + if (!this.rootStore.project.projectId) return null; + const members = this.projectMembers; + if (!members) return null; + const memberInfo: IProjectMember | null = members.find((member) => member.id === memberId) || null; + return memberInfo; + }; + + /** + * Get user information from the project members using user id + * @param memberId + * @returns + */ + getProjectMemberByUserId = (memberId: string) => { + if (!this.rootStore.project.projectId) return null; + const members = this.projectMembers; + if (!members) return null; + const memberInfo: IProjectMember | null = members.find((member) => member.member.id === memberId) || null; + return memberInfo; + }; + + /** + * fetch the project members info using workspace id and project id + * @param workspaceSlug + * @param projectId + */ + fetchProjectMembers = async (workspaceSlug: string, projectId: string) => { + try { + const membersResponse = await this.projectMemberService.fetchProjectMembers(workspaceSlug, projectId); + const _members = { + ...this.members, + [projectId]: membersResponse, + }; + runInAction(() => { + this.members = _members; + }); + } catch (error) { + console.error(error); + throw error; + } + }; + + /** + * Remove user from the project + * @param workspaceSlug + * @param projectId + * @param memberId + */ + removeMemberFromProject = async (workspaceSlug: string, projectId: string, memberId: string) => { + const originalMembers = this.projectMembers || []; + try { + runInAction(() => { + this.members = { + ...this.members, + [projectId]: this.projectMembers?.filter((member) => member.id !== memberId) || [], + }; + }); + await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberId); + await this.fetchProjectMembers(workspaceSlug, projectId); + } catch (error) { + console.log("Failed to delete project from project store"); + // revert back to original members in case of error + runInAction(() => { + this.members = { + ...this.members, + [projectId]: originalMembers, + }; + }); + } + }; + + /** + * Update member information + * @param workspaceSlug + * @param projectId + * @param memberId + * @param data + * @returns + */ + updateMember = async (workspaceSlug: string, projectId: string, memberId: string, data: Partial) => { + const originalMembers = this.projectMembers || []; + try { + runInAction(() => { + this.members = { + ...this.members, + [projectId]: (this.projectMembers || [])?.map((member) => + member.id === memberId ? { ...member, ...data } : member + ), + }; + }); + const response = await this.projectMemberService.updateProjectMember(workspaceSlug, projectId, memberId, data); + await this.fetchProjectMembers(workspaceSlug, projectId); + return response; + } catch (error) { + console.log("Failed to update project member from project store"); + // revert back to original members in case of error + runInAction(() => { + this.members = { + ...this.members, + [projectId]: originalMembers, + }; + }); + throw error; + } + }; + + deleteProjectInvitation = async () => { + try { + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/project/project_publish.store.ts b/web/store/project/project-publish.store.ts similarity index 100% rename from web/store/project/project_publish.store.ts rename to web/store/project/project-publish.store.ts diff --git a/web/store/project/project-state.store.ts b/web/store/project/project-state.store.ts new file mode 100644 index 00000000000..b0b136cb2a3 --- /dev/null +++ b/web/store/project/project-state.store.ts @@ -0,0 +1,277 @@ +import { observable, action, makeObservable, runInAction, computed } from "mobx"; +// types +import { RootStore } from "../root"; +import { IState } from "types"; +// services +import { ProjectService, ProjectStateService } from "services/project"; +import { groupBy, orderArrayBy, groupByField } from "helpers/array.helper"; +import { orderStateGroups } from "helpers/state.helper"; + +export interface IProjectStateStore { + loader: boolean; + error: any | null; + + states: { + [projectId: string]: IState[]; // projectId: states + }; + groupedProjectStates: { [groupId: string]: IState[] } | null; + projectStates: IState[] | null; + + fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise; + createState: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateState: (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => Promise; + deleteState: (workspaceSlug: string, projectId: string, stateId: string) => Promise; + markStateAsDefault: (workspaceSlug: string, projectId: string, stateId: string) => Promise; + moveStatePosition: ( + workspaceSlug: string, + projectId: string, + stateId: string, + direction: "up" | "down", + groupIndex: number + ) => Promise; +} + +export class ProjectStateStore implements IProjectStateStore { + loader: boolean = false; + error: any | null = null; + states: { + [projectId: string]: IState[]; // projectId: states + } = {}; + // root store + rootStore; + // service + projectService; + stateService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + states: observable.ref, + // computed + projectStates: computed, + groupedProjectStates: computed, + // actions + createState: action, + updateState: action, + deleteState: action, + markStateAsDefault: action, + moveStatePosition: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.stateService = new ProjectStateService(); + } + + get groupedProjectStates() { + if (!this.rootStore.project.projectId) return null; + const states = this.states[this.rootStore.project.projectId]; + if (!states) return null; + return groupByField(states, "group"); + } + + get projectStates() { + if (!this.rootStore.project.projectId) return null; + const states = this.states[this.rootStore.project.projectId]; + if (!states) return null; + return states; + } + + fetchProjectStates = async (workspaceSlug: string, projectId: string) => { + try { + const states = await this.stateService.getStates(workspaceSlug, projectId); + runInAction(() => { + this.states = { + ...this.states, + [projectId]: states, + }; + }); + return states; + } catch (error) { + throw error; + } + }; + + getProjectStateById = (stateId: string) => { + if (!this.rootStore.project.projectId) return null; + const states = this.states[this.rootStore.project.projectId]; + if (!states) return null; + const stateInfo: IState | null = states.find((state) => state.id === stateId) || null; + return stateInfo; + }; + + createState = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.stateService.createState( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser! + ); + + runInAction(() => { + this.states = { + ...this.states, + [projectId]: [...this.states?.[projectId], response], + }; + }); + + return response; + } catch (error) { + console.log("Failed to create state from project store"); + throw error; + } + }; + + updateState = async (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => { + const originalStates = this.states; + + try { + runInAction(() => { + this.states = { + ...this.states, + [projectId]: [ + ...this.states?.[projectId].map((state) => { + if (state.id === stateId) { + return { ...state, ...data }; + } + return state; + }), + ], + }; + }); + + const response = await this.stateService.patchState( + workspaceSlug, + projectId, + stateId, + data, + this.rootStore.user.currentUser! + ); + + return response; + } catch (error) { + console.log("Failed to update state from project store"); + runInAction(() => { + this.states = originalStates; + }); + throw error; + } + }; + + deleteState = async (workspaceSlug: string, projectId: string, stateId: string) => { + const originalStates = this.states; + + try { + runInAction(() => { + this.states = { + ...this.states, + [projectId]: [ + ...this.states?.[projectId].filter((state) => { + if (state.id !== stateId) { + return stateId; + } + }), + ], + }; + }); + + // deleting using api + await this.stateService.deleteState(workspaceSlug, projectId, stateId, this.rootStore.user.currentUser!); + } catch (error) { + console.log("Failed to delete state from project store"); + // reverting back to original label list + runInAction(() => { + this.states = originalStates; + }); + throw error; + } + }; + + markStateAsDefault = async (workspaceSlug: string, projectId: string, stateId: string) => { + const originalStates = this.states; + try { + const currentDefaultStateIds = this.projectStates?.filter((s) => s.default).map((state) => state.id); + + runInAction(() => { + this.states = { + ...this.states, + [projectId]: [ + ...this.states[projectId].map((state) => { + if (currentDefaultStateIds?.includes(state.id)) { + return { ...state, default: false }; + } else if (state.id === stateId) { + return { ...state, default: true }; + } + return state; + }), + ], + }; + }); + + // updating using api + await this.stateService.markDefault(workspaceSlug, projectId, stateId); + } catch (error) { + console.log("Failed to mark state as default"); + runInAction(() => { + this.states = originalStates; + }); + throw error; + } + }; + + moveStatePosition = async ( + workspaceSlug: string, + projectId: string, + stateId: string, + direction: "up" | "down", + groupIndex: number + ) => { + const SEQUENCE_GAP = 15000; + const originalStates = this.states; + + try { + let newSequence = SEQUENCE_GAP; + const states = this.projectStates || []; + const selectedState = states?.find((state) => state.id === stateId); + const groupStates = states?.filter((state) => state.group === selectedState?.group); + const groupLength = groupStates.length; + if (direction === "up") { + if (groupIndex === 1) newSequence = groupStates[0].sequence - SEQUENCE_GAP; + else newSequence = (groupStates[groupIndex - 2].sequence + groupStates[groupIndex - 1].sequence) / 2; + } else { + if (groupIndex === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + SEQUENCE_GAP; + else newSequence = (groupStates[groupIndex + 2].sequence + groupStates[groupIndex + 1].sequence) / 2; + } + + const newStateList = states?.map((state) => { + if (state.id === stateId) return { ...state, sequence: newSequence }; + return state; + }); + + // updating using api + runInAction(() => { + this.states = { + ...this.states, + [projectId]: newStateList, + }; + }); + + await this.stateService.patchState( + workspaceSlug, + projectId, + stateId, + { sequence: newSequence }, + this.rootStore.user.currentUser! + ); + } catch (err) { + console.log("Failed to move state position"); + // reverting back to old state group if api fails + runInAction(() => { + this.states = originalStates; + }); + } + }; +} diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 976654ee9ee..9997de5c15c 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -1,7 +1,7 @@ import { observable, action, computed, makeObservable, runInAction } from "mobx"; // types import { RootStore } from "../root"; -import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState, IEstimate } from "types"; +import { IProject, IIssueLabels, IEstimate } from "types"; // services import { ProjectService, ProjectStateService, ProjectEstimateService } from "services/project"; import { IssueService, IssueLabelService } from "services/issue"; @@ -16,26 +16,17 @@ export interface IProjectStore { project_details: { [projectId: string]: IProject; // projectId: project Info }; - states: { - [projectId: string]: IStateResponse; // project_id: states - } | null; labels: { [projectId: string]: IIssueLabels[] | null; // project_id: labels } | null; - members: { - [projectId: string]: IProjectMember[] | null; // project_id: members - } | null; estimates: { [projectId: string]: IEstimate[] | null; // project_id: members } | null; // computed searchedProjects: IProject[]; - workspaceProjects: IProject[]; - projectStatesByGroups: IStateResponse | null; - projectStates: IState[] | null; + workspaceProjects: IProject[] | null; projectLabels: IIssueLabels[] | null; - projectMembers: IProjectMember[] | null; projectEstimates: IEstimate[] | null; joinedProjects: IProject[]; @@ -44,21 +35,16 @@ export interface IProjectStore { currentProjectDetails: IProject | undefined; // actions - setProjectId: (projectId: string) => void; + setProjectId: (projectId: string | null) => void; setSearchQuery: (query: string) => void; getProjectById: (workspaceSlug: string, projectId: string) => IProject | null; - getProjectStateById: (stateId: string) => IState | null; getProjectLabelById: (labelId: string) => IIssueLabels | null; - getProjectMemberById: (memberId: string) => IProjectMember | null; - getProjectMemberByUserId: (memberId: string) => IProjectMember | null; getProjectEstimateById: (estimateId: string) => IEstimate | null; fetchProjects: (workspaceSlug: string) => Promise; fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise; - fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise; fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise; - fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise; fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise; addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise; @@ -72,15 +58,6 @@ export interface IProjectStore { createProject: (workspaceSlug: string, data: any) => Promise; updateProject: (workspaceSlug: string, projectId: string, data: Partial) => Promise; deleteProject: (workspaceSlug: string, projectId: string) => Promise; - - // write operations - removeMemberFromProject: (workspaceSlug: string, projectId: string, memberId: string) => Promise; - updateMember: ( - workspaceSlug: string, - projectId: string, - memberId: string, - data: Partial - ) => Promise; } export class ProjectStore implements IProjectStore { @@ -93,15 +70,9 @@ export class ProjectStore implements IProjectStore { project_details: { [projectId: string]: IProject; // projectId: project } = {}; - states: { - [projectId: string]: IStateResponse; // projectId: states - } | null = {}; labels: { [projectId: string]: IIssueLabels[]; // projectId: labels } | null = {}; - members: { - [projectId: string]: IProjectMember[]; // projectId: members - } | null = {}; estimates: { [projectId: string]: IEstimate[]; // projectId: estimates } | null = {}; @@ -125,18 +96,13 @@ export class ProjectStore implements IProjectStore { projectId: observable.ref, projects: observable.ref, project_details: observable.ref, - states: observable.ref, labels: observable.ref, - members: observable.ref, estimates: observable.ref, // computed searchedProjects: computed, workspaceProjects: computed, - projectStatesByGroups: computed, - projectStates: computed, projectLabels: computed, - projectMembers: computed, projectEstimates: computed, currentProjectDetails: computed, @@ -151,14 +117,10 @@ export class ProjectStore implements IProjectStore { fetchProjectDetails: action, getProjectById: action, - getProjectStateById: action, getProjectLabelById: action, - getProjectMemberById: action, getProjectEstimateById: action, - fetchProjectStates: action, fetchProjectLabels: action, - fetchProjectMembers: action, fetchProjectEstimates: action, addProjectToFavorites: action, @@ -169,10 +131,6 @@ export class ProjectStore implements IProjectStore { createProject: action, updateProject: action, leaveProject: action, - - // write operations - removeMemberFromProject: action, - updateMember: action, }); this.rootStore = _rootStore; @@ -198,8 +156,10 @@ export class ProjectStore implements IProjectStore { } get workspaceProjects() { - if (!this.rootStore.workspace.workspaceSlug) return []; - return this.projects?.[this.rootStore.workspace.workspaceSlug]; + if (!this.rootStore.workspace.workspaceSlug) return null; + const projects = this.projects[this.rootStore.workspace.workspaceSlug]; + if (!projects) return null; + return projects; } get currentProjectDetails() { @@ -217,42 +177,19 @@ export class ProjectStore implements IProjectStore { return this.projects?.[this.rootStore.workspace.workspaceSlug]?.filter((p) => p.is_favorite); } - get projectStatesByGroups() { - if (!this.projectId) return null; - return this.states?.[this.projectId] || null; - } - - get projectStates() { - if (!this.projectId) return null; - const stateByGroups: IStateResponse | null = this.projectStatesByGroups; - if (!stateByGroups) return null; - const _states: IState[] = []; - Object.keys(stateByGroups).forEach((_stateGroup: string) => { - stateByGroups[_stateGroup].map((state) => { - _states.push(state); - }); - }); - return _states.length > 0 ? _states : null; - } - get projectLabels() { if (!this.projectId) return null; return this.labels?.[this.projectId] || null; } - get projectMembers() { - if (!this.projectId) return null; - return this.members?.[this.projectId] || null; - } - get projectEstimates() { if (!this.projectId) return null; return this.estimates?.[this.projectId] || null; } // actions - setProjectId = (projectId: string) => { - this.projectId = projectId ?? null; + setProjectId = (projectId: string | null) => { + this.projectId = projectId; }; setSearchQuery = (query: string) => { @@ -304,14 +241,6 @@ export class ProjectStore implements IProjectStore { return projectInfo; }; - getProjectStateById = (stateId: string) => { - if (!this.projectId) return null; - const states = this.projectStates; - if (!states) return null; - const stateInfo: IState | null = states.find((state) => state.id === stateId) || null; - return stateInfo; - }; - getProjectLabelById = (labelId: string) => { if (!this.projectId) return null; const labels = this.projectLabels; @@ -320,22 +249,6 @@ export class ProjectStore implements IProjectStore { return labelInfo; }; - getProjectMemberById = (memberId: string) => { - if (!this.projectId) return null; - const members = this.projectMembers; - if (!members) return null; - const memberInfo: IProjectMember | null = members.find((member) => member.id === memberId) || null; - return memberInfo; - }; - - getProjectMemberByUserId = (memberId: string) => { - if (!this.projectId) return null; - const members = this.projectMembers; - if (!members) return null; - const memberInfo: IProjectMember | null = members.find((member) => member.member.id === memberId) || null; - return memberInfo; - }; - getProjectEstimateById = (estimateId: string) => { if (!this.projectId) return null; const estimates = this.projectEstimates; @@ -344,29 +257,6 @@ export class ProjectStore implements IProjectStore { return estimateInfo; }; - fetchProjectStates = async (workspaceSlug: string, projectId: string) => { - try { - this.loader = true; - this.error = null; - - const stateResponse = await this.stateService.getStates(workspaceSlug, projectId); - const _states = { - ...this.states, - [projectId]: stateResponse, - }; - - runInAction(() => { - this.states = _states; - this.loader = false; - this.error = null; - }); - } catch (error) { - console.error(error); - this.loader = false; - this.error = error; - } - }; - fetchProjectLabels = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; @@ -389,29 +279,6 @@ export class ProjectStore implements IProjectStore { } }; - fetchProjectMembers = async (workspaceSlug: string, projectId: string) => { - try { - this.loader = true; - this.error = null; - - const membersResponse = await this.projectService.fetchProjectMembers(workspaceSlug, projectId); - const _members = { - ...this.members, - [projectId]: membersResponse, - }; - - runInAction(() => { - this.members = _members; - this.loader = false; - this.error = null; - }); - } catch (error) { - console.error(error); - this.loader = false; - this.error = error; - } - }; - fetchProjectEstimates = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; @@ -638,58 +505,4 @@ export class ProjectStore implements IProjectStore { console.log("Failed to delete project from project store"); } }; - - removeMemberFromProject = async (workspaceSlug: string, projectId: string, memberId: string) => { - const originalMembers = this.projectMembers || []; - - runInAction(() => { - this.members = { - ...this.members, - [projectId]: this.projectMembers?.filter((member) => member.id !== memberId) || [], - }; - }); - - try { - await this.projectService.deleteProjectMember(workspaceSlug, projectId, memberId); - await this.fetchProjectMembers(workspaceSlug, projectId); - } catch (error) { - console.log("Failed to delete project from project store"); - // revert back to original members in case of error - runInAction(() => { - this.members = { - ...this.members, - [projectId]: originalMembers, - }; - }); - } - }; - - updateMember = async (workspaceSlug: string, projectId: string, memberId: string, data: Partial) => { - const originalMembers = this.projectMembers || []; - - runInAction(() => { - this.members = { - ...this.members, - [projectId]: (this.projectMembers || [])?.map((member) => - member.id === memberId ? { ...member, ...data } : member - ), - }; - }); - - try { - const response = await this.projectService.updateProjectMember(workspaceSlug, projectId, memberId, data); - await this.fetchProjectMembers(workspaceSlug, projectId); - return response; - } catch (error) { - console.log("Failed to update project member from project store"); - // revert back to original members in case of error - runInAction(() => { - this.members = { - ...this.members, - [projectId]: originalMembers, - }; - }); - throw error; - } - }; } diff --git a/web/store/project/project_state.store.ts b/web/store/project/project_state.store.ts deleted file mode 100644 index 56fc0c203ca..00000000000 --- a/web/store/project/project_state.store.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { observable, action, makeObservable, runInAction } from "mobx"; -// types -import { RootStore } from "../root"; -import { IState } from "types"; -// services -import { ProjectService, ProjectStateService } from "services/project"; -import { groupBy, orderArrayBy } from "helpers/array.helper"; -import { orderStateGroups } from "helpers/state.helper"; - -export interface IProjectStateStore { - loader: boolean; - error: any | null; - - // states - createState: (workspaceSlug: string, projectId: string, data: Partial) => Promise; - updateState: (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => Promise; - deleteState: (workspaceSlug: string, projectId: string, stateId: string) => Promise; - markStateAsDefault: (workspaceSlug: string, projectId: string, stateId: string) => Promise; - moveStatePosition: ( - workspaceSlug: string, - projectId: string, - stateId: string, - direction: "up" | "down", - groupIndex: number - ) => Promise; -} - -export class ProjectStateStore implements IProjectStateStore { - loader: boolean = false; - error: any | null = null; - - // root store - rootStore; - // service - projectService; - stateService; - - constructor(_rootStore: RootStore) { - makeObservable(this, { - // observable - loader: observable, - error: observable, - - // states - createState: action, - updateState: action, - deleteState: action, - markStateAsDefault: action, - moveStatePosition: action, - }); - - this.rootStore = _rootStore; - this.projectService = new ProjectService(); - this.stateService = new ProjectStateService(); - } - - createState = async (workspaceSlug: string, projectId: string, data: Partial) => { - try { - const response = await this.stateService.createState( - workspaceSlug, - projectId, - data, - this.rootStore.user.currentUser! - ); - - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [response.group]: [...(this.rootStore.project.states?.[projectId]?.[response.group] || []), response], - }, - }; - }); - - return response; - } catch (error) { - console.log("Failed to create state from project store"); - throw error; - } - }; - - updateState = async (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => { - const originalStates = this.rootStore.project.states || {}; - - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [data.group as string]: (this.rootStore.project.states?.[projectId]?.[data.group as string] || []).map( - (state) => (state.id === stateId ? { ...state, ...data } : state) - ), - }, - }; - }); - - try { - const response = await this.stateService.patchState( - workspaceSlug, - projectId, - stateId, - data, - this.rootStore.user.currentUser! - ); - - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [response.group]: (this.rootStore.project.states?.[projectId]?.[response.group] || []).map((state) => - state.id === stateId ? { ...state, ...response } : state - ), - }, - }; - }); - - return response; - } catch (error) { - console.log("Failed to update state from project store"); - runInAction(() => { - this.rootStore.project.states = originalStates; - }); - throw error; - } - }; - - deleteState = async (workspaceSlug: string, projectId: string, stateId: string) => { - const originalStates = this.rootStore.project.projectStates; - - try { - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [originalStates?.[0]?.group || ""]: ( - this.rootStore.project.states?.[projectId]?.[originalStates?.[0]?.group || ""] || [] - ).filter((state) => state.id !== stateId), - }, - }; - }); - - // deleting using api - await this.stateService.deleteState(workspaceSlug, projectId, stateId, this.rootStore.user.currentUser!); - } catch (error) { - console.log("Failed to delete state from project store"); - // reverting back to original label list - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: { - ...this.rootStore.project.states?.[projectId], - [originalStates?.[0]?.group || ""]: originalStates || [], - }, - }; - }); - } - }; - - markStateAsDefault = async (workspaceSlug: string, projectId: string, stateId: string) => { - const states = this.rootStore.project.projectStates; - const currentDefaultState = states?.find((state) => state.default); - - let newStateList = - states?.map((state) => { - if (state.id === stateId) return { ...state, default: true }; - if (state.id === currentDefaultState?.id) return { ...state, default: false }; - return state; - }) ?? []; - newStateList = orderArrayBy(newStateList, "sequence", "ascending"); - - const newOrderedStateGroups = orderStateGroups(groupBy(newStateList, "group")); - const oldOrderedStateGroup = this.rootStore.project.states?.[projectId] || {}; // for reverting back to old state group if api fails - - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: newOrderedStateGroups || {}, - }; - }); - - // updating using api - try { - this.stateService.patchState( - workspaceSlug, - projectId, - stateId, - { default: true }, - this.rootStore.user.currentUser! - ); - - if (currentDefaultState) - this.stateService.patchState( - workspaceSlug, - projectId, - currentDefaultState.id, - { default: false }, - this.rootStore.user.currentUser! - ); - } catch (err) { - console.log("Failed to mark state as default"); - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: oldOrderedStateGroup, - }; - }); - } - }; - - moveStatePosition = async ( - workspaceSlug: string, - projectId: string, - stateId: string, - direction: "up" | "down", - groupIndex: number - ) => { - const SEQUENCE_GAP = 15000; - let newSequence = SEQUENCE_GAP; - - const states = this.rootStore.project.projectStates || []; - const groupedStates = groupBy(states || [], "group"); - - const selectedState = states?.find((state) => state.id === stateId); - const groupStates = states?.filter((state) => state.group === selectedState?.group); - const groupLength = groupStates.length; - - if (direction === "up") { - if (groupIndex === 1) newSequence = groupStates[0].sequence - SEQUENCE_GAP; - else newSequence = (groupStates[groupIndex - 2].sequence + groupStates[groupIndex - 1].sequence) / 2; - } else { - if (groupIndex === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + SEQUENCE_GAP; - else newSequence = (groupStates[groupIndex + 2].sequence + groupStates[groupIndex + 1].sequence) / 2; - } - - const newStateList = states?.map((state) => { - if (state.id === stateId) return { ...state, sequence: newSequence }; - return state; - }); - const newOrderedStateGroups = orderStateGroups( - groupBy(orderArrayBy(newStateList, "sequence", "ascending"), "group") - ); - - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: newOrderedStateGroups || {}, - }; - }); - - // updating using api - try { - await this.stateService.patchState( - workspaceSlug, - projectId, - stateId, - { sequence: newSequence }, - this.rootStore.user.currentUser! - ); - } catch (err) { - console.log("Failed to move state position"); - // reverting back to old state group if api fails - runInAction(() => { - this.rootStore.project.states = { - ...this.rootStore.project.states, - [projectId]: groupedStates, - }; - }); - } - }; -} diff --git a/web/store/root.ts b/web/store/root.ts index 9af4db4924b..3b03bf9c872 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -1,5 +1,6 @@ import { enableStaticRendering } from "mobx-react-lite"; // store imports +import AppConfigStore, { IAppConfigStore } from "./app-config.store"; import CommandPaletteStore, { ICommandPaletteStore } from "./command-palette.store"; import UserStore, { IUserStore } from "store/user.store"; import ThemeStore, { IThemeStore } from "store/theme.store"; @@ -18,7 +19,14 @@ import { IIssueQuickAddStore, IssueQuickAddStore, } from "store/issue"; -import { IWorkspaceFilterStore, IWorkspaceStore, WorkspaceFilterStore, WorkspaceStore } from "store/workspace"; +import { + IWorkspaceFilterStore, + IWorkspaceStore, + WorkspaceFilterStore, + WorkspaceStore, + WorkspaceMemberStore, + IWorkspaceMemberStore, +} from "store/workspace"; import { IProjectPublishStore, IProjectStore, @@ -30,6 +38,8 @@ import { ProjectLabelStore, ProjectEstimatesStore, IProjectEstimateStore, + ProjectMemberStore, + IProjectMemberStore, } from "store/project"; import { IModuleFilterStore, @@ -107,16 +117,20 @@ enableStaticRendering(typeof window === "undefined"); export class RootStore { user: IUserStore; theme: IThemeStore; - + appConfig: IAppConfigStore; commandPalette: ICommandPaletteStore; + workspace: IWorkspaceStore; workspaceFilter: IWorkspaceFilterStore; + workspaceMember: IWorkspaceMemberStore; projectPublish: IProjectPublishStore; project: IProjectStore; projectState: IProjectStateStore; projectLabel: IProjectLabelStore; projectEstimates: IProjectEstimateStore; + projectMember: IProjectMemberStore; + issue: IIssueStore; module: IModuleStore; @@ -167,18 +181,21 @@ export class RootStore { mentionsStore: IMentionsStore; constructor() { + this.appConfig = new AppConfigStore(this); this.commandPalette = new CommandPaletteStore(this); this.user = new UserStore(this); this.theme = new ThemeStore(this); this.workspace = new WorkspaceStore(this); this.workspaceFilter = new WorkspaceFilterStore(this); + this.workspaceMember = new WorkspaceMemberStore(this); this.project = new ProjectStore(this); this.projectState = new ProjectStateStore(this); this.projectLabel = new ProjectLabelStore(this); this.projectEstimates = new ProjectEstimatesStore(this); this.projectPublish = new ProjectPublishStore(this); + this.projectMember = new ProjectMemberStore(this); this.module = new ModuleStore(this); this.moduleIssue = new ModuleIssueStore(this); diff --git a/web/store/user.store.ts b/web/store/user.store.ts index 80cafdf44d8..c1d91904f29 100644 --- a/web/store/user.store.ts +++ b/web/store/user.store.ts @@ -1,7 +1,7 @@ // mobx import { action, observable, runInAction, makeObservable, computed } from "mobx"; // services -import { ProjectService } from "services/project"; +import { ProjectMemberService, ProjectService } from "services/project"; import { UserService } from "services/user.service"; import { WorkspaceService } from "services/workspace.service"; // interfaces @@ -81,6 +81,7 @@ class UserStore implements IUserStore { userService; workspaceService; projectService; + projectMemberService; constructor(_rootStore: RootStore) { makeObservable(this, { @@ -115,6 +116,7 @@ class UserStore implements IUserStore { this.userService = new UserService(); this.workspaceService = new WorkspaceService(); this.projectService = new ProjectService(); + this.projectMemberService = new ProjectMemberService(); } get currentWorkspaceMemberInfo() { @@ -219,7 +221,7 @@ class UserStore implements IUserStore { fetchUserProjectInfo = async (workspaceSlug: string, projectId: string) => { try { - const response = await this.projectService.projectMemberMe(workspaceSlug, projectId); + const response = await this.projectMemberService.projectMemberMe(workspaceSlug, projectId); runInAction(() => { this.projectMemberInfo = { diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts index 887bd0b0d67..e9f90d979bf 100644 --- a/web/store/workspace/index.ts +++ b/web/store/workspace/index.ts @@ -1,2 +1,3 @@ export * from "./workspace_filters.store"; export * from "./workspace.store"; +export * from "./workspace-member.store"; diff --git a/web/store/workspace/workspace-member.store.ts b/web/store/workspace/workspace-member.store.ts new file mode 100644 index 00000000000..01002373695 --- /dev/null +++ b/web/store/workspace/workspace-member.store.ts @@ -0,0 +1,274 @@ +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { RootStore } from "../root"; +// types +import { IUser, IWorkspaceMember, IWorkspaceMemberInvitation, IWorkspaceBulkInviteFormData } from "types"; +// services +import { WorkspaceService } from "services/workspace.service"; + +export interface IWorkspaceMemberStore { + // states + loader: boolean; + error: any | null; + + // observables + members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[] + memberInvitations: { [workspaceSlug: string]: IWorkspaceMemberInvitation[] }; + // actions + fetchWorkspaceMembers: (workspaceSlug: string) => Promise; + fetchWorkspaceMemberInvitations: (workspaceSlug: string) => Promise; + updateMember: (workspaceSlug: string, memberId: string, data: Partial) => Promise; + removeMember: (workspaceSlug: string, memberId: string) => Promise; + inviteMembersToWorkspace: (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => Promise; + deleteWorkspaceInvitation: (workspaceSlug: string, memberId: string) => Promise; + // computed + workspaceMembers: IWorkspaceMember[] | null; + workspaceMemberInvitations: IWorkspaceMemberInvitation[] | null; + workspaceMembersWithInvitations: any[] | null; +} + +export class WorkspaceMemberStore implements IWorkspaceMemberStore { + // states + loader: boolean = false; + error: any | null = null; + // observables + members: { [workspaceSlug: string]: IWorkspaceMember[] } = {}; + memberInvitations: { [workspaceSlug: string]: IWorkspaceMemberInvitation[] } = {}; + // services + workspaceService; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + members: observable.ref, + memberInvitations: observable.ref, + // actions + fetchWorkspaceMembers: action, + fetchWorkspaceMemberInvitations: action, + updateMember: action, + removeMember: action, + inviteMembersToWorkspace: action, + deleteWorkspaceInvitation: action, + // computed + workspaceMembers: computed, + workspaceMemberInvitations: computed, + workspaceMembersWithInvitations: computed, + }); + + this.rootStore = _rootStore; + this.workspaceService = new WorkspaceService(); + } + + /** + * computed value of workspace members using the workspace slug from the store + */ + get workspaceMembers() { + if (!this.rootStore.workspace.workspaceSlug) return null; + const members = this.members?.[this.rootStore.workspace.workspaceSlug]; + if (!members) return null; + return members; + } + + /** + * Computed value of workspace member invitations using workspace slug from store + */ + get workspaceMemberInvitations() { + if (!this.rootStore.workspace.workspaceSlug) return null; + const invitations = this.memberInvitations?.[this.rootStore.workspace.workspaceSlug]; + if (!invitations) return null; + return invitations; + } + + /** + * computed value provides the members information including the invitations. + */ + get workspaceMembersWithInvitations() { + if (!this.workspaceMembers || !this.workspaceMemberInvitations) return null; + return [ + ...(this.workspaceMemberInvitations?.map((item) => ({ + id: item.id, + memberId: item.id, + avatar: "", + first_name: item.email, + last_name: "", + email: item.email, + display_name: item.email, + role: item.role, + status: item.accepted, + member: false, + accountCreated: item.accepted, + })) || []), + ...(this.workspaceMembers?.map((item) => ({ + id: item.id, + memberId: item.member?.id, + avatar: item.member?.avatar, + first_name: item.member?.first_name, + last_name: item.member?.last_name, + email: item.member?.email, + display_name: item.member?.display_name, + role: item.role, + status: true, + member: true, + accountCreated: true, + })) || []), + ]; + } + + /** + * fetch workspace members using workspace slug + * @param workspaceSlug + */ + fetchWorkspaceMembers = async (workspaceSlug: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const membersResponse = await this.workspaceService.fetchWorkspaceMembers(workspaceSlug); + + runInAction(() => { + this.members = { + ...this.members, + [workspaceSlug]: membersResponse, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + } + }; + + /** + * fetching workspace member invitations + * @param workspaceSlug + * @returns + */ + fetchWorkspaceMemberInvitations = async (workspaceSlug: string) => { + try { + const membersInvitations = await this.workspaceService.workspaceInvitations(workspaceSlug); + runInAction(() => { + this.memberInvitations = { + ...this.memberInvitations, + [workspaceSlug]: membersInvitations, + }; + }); + return membersInvitations; + } catch (error) { + throw error; + } + }; + + /** + * invite members to the workspace using emails + * @param workspaceSlug + * @param data + */ + inviteMembersToWorkspace = async (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => { + try { + await this.workspaceService.inviteWorkspace(workspaceSlug, data, this.rootStore.user.currentUser as IUser); + await this.fetchWorkspaceMemberInvitations(workspaceSlug); + } catch (error) { + throw error; + } + }; + + /** + * delete the workspace invitation + * @param workspaceSlug + * @param memberId + */ + deleteWorkspaceInvitation = async (workspaceSlug: string, memberId: string) => { + try { + runInAction(() => { + this.memberInvitations = { + ...this.memberInvitations, + [workspaceSlug]: [...this.memberInvitations[workspaceSlug].filter((inv) => inv.id !== memberId)], + }; + }); + await this.workspaceService.deleteWorkspaceInvitations(workspaceSlug.toString(), memberId); + } catch (error) { + throw error; + } + }; + + /** + * update workspace member using workspace slug and member id and data + * @param workspaceSlug + * @param memberId + * @param data + */ + updateMember = async (workspaceSlug: string, memberId: string, data: Partial) => { + const members = this.members?.[workspaceSlug]; + members?.map((m) => (m.id === memberId ? { ...m, ...data } : m)); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data); + + runInAction(() => { + this.loader = false; + this.error = null; + this.members = { + ...this.members, + [workspaceSlug]: members, + }; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + /** + * remove workspace member using workspace slug and member id + * @param workspaceSlug + * @param memberId + */ + removeMember = async (workspaceSlug: string, memberId: string) => { + const members = this.members?.[workspaceSlug]; + members?.filter((m) => m.id !== memberId); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId); + + runInAction(() => { + this.loader = false; + this.error = null; + this.members = { + ...this.members, + [workspaceSlug]: members, + }; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; +} diff --git a/web/store/workspace/workspace.store.ts b/web/store/workspace/workspace.store.ts index 6891da72c89..1092ec33b4e 100644 --- a/web/store/workspace/workspace.store.ts +++ b/web/store/workspace/workspace.store.ts @@ -16,7 +16,6 @@ export interface IWorkspaceStore { workspaceSlug: string | null; workspaces: IWorkspace[] | undefined; labels: { [workspaceSlug: string]: IIssueLabels[] }; // workspaceSlug: labels[] - members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[] // actions setWorkspaceSlug: (workspaceSlug: string) => void; @@ -24,22 +23,16 @@ export interface IWorkspaceStore { getWorkspaceLabelById: (workspaceSlug: string, labelId: string) => IIssueLabels | null; fetchWorkspaces: () => Promise; fetchWorkspaceLabels: (workspaceSlug: string) => Promise; - fetchWorkspaceMembers: (workspaceSlug: string) => Promise; // workspace write operations createWorkspace: (data: Partial) => Promise; updateWorkspace: (workspaceSlug: string, data: Partial) => Promise; deleteWorkspace: (workspaceSlug: string) => Promise; - // members write operations - updateMember: (workspaceSlug: string, memberId: string, data: Partial) => Promise; - removeMember: (workspaceSlug: string, memberId: string) => Promise; - // computed currentWorkspace: IWorkspace | null; workspacesCreateByCurrentUser: IWorkspace[] | null; workspaceLabels: IIssueLabels[] | null; - workspaceMembers: IWorkspaceMember[] | null; } export class WorkspaceStore implements IWorkspaceStore { @@ -72,7 +65,6 @@ export class WorkspaceStore implements IWorkspaceStore { workspaceSlug: observable.ref, workspaces: observable.ref, labels: observable.ref, - members: observable.ref, // actions setWorkspaceSlug: action, @@ -80,21 +72,15 @@ export class WorkspaceStore implements IWorkspaceStore { getWorkspaceLabelById: action, fetchWorkspaces: action, fetchWorkspaceLabels: action, - fetchWorkspaceMembers: action, // workspace write operations createWorkspace: action, updateWorkspace: action, deleteWorkspace: action, - // members write operations - updateMember: action, - removeMember: action, - // computed currentWorkspace: computed, workspaceLabels: computed, - workspaceMembers: computed, }); this.rootStore = _rootStore; @@ -135,15 +121,6 @@ export class WorkspaceStore implements IWorkspaceStore { return _labels && Object.keys(_labels).length > 0 ? _labels : []; } - /** - * computed value of workspace members using the workspace slug from the store - */ - get workspaceMembers() { - if (!this.workspaceSlug) return []; - const _members = this.members?.[this.workspaceSlug]; - return _members && Object.keys(_members).length > 0 ? _members : []; - } - /** * set workspace slug in the store * @param workspaceSlug @@ -224,35 +201,6 @@ export class WorkspaceStore implements IWorkspaceStore { } }; - /** - * fetch workspace members using workspace slug - * @param workspaceSlug - */ - fetchWorkspaceMembers = async (workspaceSlug: string) => { - try { - runInAction(() => { - this.loader = true; - this.error = null; - }); - - const membersResponse = await this.workspaceService.fetchWorkspaceMembers(workspaceSlug); - - runInAction(() => { - this.members = { - ...this.members, - [workspaceSlug]: membersResponse, - }; - this.loader = false; - this.error = null; - }); - } catch (error) { - runInAction(() => { - this.loader = false; - this.error = error; - }); - } - }; - /** * create workspace using the workspace data * @param data @@ -351,75 +299,4 @@ export class WorkspaceStore implements IWorkspaceStore { throw error; } }; - - /** - * update workspace member using workspace slug and member id and data - * @param workspaceSlug - * @param memberId - * @param data - */ - updateMember = async (workspaceSlug: string, memberId: string, data: Partial) => { - const members = this.members?.[workspaceSlug]; - members?.map((m) => (m.id === memberId ? { ...m, ...data } : m)); - - try { - runInAction(() => { - this.loader = true; - this.error = null; - }); - - await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data); - - runInAction(() => { - this.loader = false; - this.error = null; - this.members = { - ...this.members, - [workspaceSlug]: members, - }; - }); - } catch (error) { - runInAction(() => { - this.loader = false; - this.error = error; - }); - - throw error; - } - }; - - /** - * remove workspace member using workspace slug and member id - * @param workspaceSlug - * @param memberId - */ - removeMember = async (workspaceSlug: string, memberId: string) => { - const members = this.members?.[workspaceSlug]; - members?.filter((m) => m.id !== memberId); - - try { - runInAction(() => { - this.loader = true; - this.error = null; - }); - - await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId); - - runInAction(() => { - this.loader = false; - this.error = null; - this.members = { - ...this.members, - [workspaceSlug]: members, - }; - }); - } catch (error) { - runInAction(() => { - this.loader = false; - this.error = error; - }); - - throw error; - } - }; } diff --git a/web/styles/globals.css b/web/styles/globals.css index e3042593606..8f7ebe624cc 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -53,6 +53,8 @@ --color-text-300: 82, 82, 82; /* tertiary text */ --color-text-400: 163, 163, 163; /* placeholder text */ + --color-scrollbar: 163, 163, 163; /* scrollbar thumb */ + --color-border-100: 245, 245, 245; /* subtle border= 1 */ --color-border-200: 229, 229, 229; /* subtle border- 2 */ --color-border-300: 212, 212, 212; /* strong border- 1 */ @@ -117,6 +119,8 @@ --color-text-300: 82, 82, 82; /* tertiary text */ --color-text-400: 163, 163, 163; /* placeholder text */ + --color-scrollbar: 163, 163, 163; /* scrollbar thumb */ + --color-border-100: 245, 245, 245; /* subtle border= 1 */ --color-border-200: 229, 229, 229; /* subtle border- 2 */ --color-border-300: 212, 212, 212; /* strong border- 1 */ @@ -129,6 +133,8 @@ --color-text-300: 58, 58, 58; /* tertiary text */ --color-text-400: 115, 115, 115; /* placeholder text */ + --color-scrollbar: 115, 115, 115; /* scrollbar thumb */ + --color-border-100: 34, 34, 34; /* subtle border= 1 */ --color-border-200: 38, 38, 38; /* subtle border- 2 */ --color-border-300: 46, 46, 46; /* strong border- 1 */ @@ -160,6 +166,8 @@ --color-text-300: 115, 115, 115; /* tertiary text */ --color-text-400: 82, 82, 82; /* placeholder text */ + --color-scrollbar: 82, 82, 82; /* scrollbar thumb */ + --color-border-100: 34, 34, 34; /* subtle border= 1 */ --color-border-200: 38, 38, 38; /* subtle border- 2 */ --color-border-300: 46, 46, 46; /* strong border- 1 */ @@ -172,6 +180,8 @@ --color-text-300: 212, 212, 212; /* tertiary text */ --color-text-400: 115, 115, 115; /* placeholder text */ + --color-scrollbar: 115, 115, 115; /* scrollbar thumb */ + --color-border-100: 245, 245, 245; /* subtle border= 1 */ --color-border-200: 229, 229, 229; /* subtle border- 2 */ --color-border-300: 212, 212, 212; /* strong border- 1 */ @@ -211,9 +221,9 @@ --color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */ --color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */ - --color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */ - --color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */ - --color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */ + --color-sidebar-border-200: var(--color-border-200); /* subtle sidebar border- 2 */ + --color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */ + --color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */ } } @@ -256,7 +266,7 @@ body { .horizontal-scroll-enable::-webkit-scrollbar-thumb { border-radius: 5px; - background-color: rgba(var(--color-background-80)); + background-color: rgba(var(--color-scrollbar)); } .vertical-scroll-enable::-webkit-scrollbar { diff --git a/web/styles/table.css b/web/styles/table.css index ad88fd10ec8..bce7e4683ad 100644 --- a/web/styles/table.css +++ b/web/styles/table.css @@ -92,7 +92,7 @@ transform: translateY(-50%); } -.tableWrapper .tableControls .columnsControl > button { +.tableWrapper .tableControls .columnsControl .columnsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); width: 30px; @@ -104,14 +104,14 @@ transform: translateX(-50%); } -.tableWrapper .tableControls .rowsControl > button { +.tableWrapper .tableControls .rowsControl .rowsControlDiv { color: white; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E"); height: 30px; width: 15px; } -.tableWrapper .tableControls button { +.tableWrapper .tableControls .rowsControlDiv { background-color: rgba(var(--color-primary-100)); border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; @@ -124,6 +124,18 @@ cursor: pointer; } +.tableWrapper .tableControls .columnsControlDiv { + background-color: rgba(var(--color-primary-100)); + border: 1px solid rgba(var(--color-border-200)); + border-radius: 2px; + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: center; + transition: transform ease-out 100ms, background-color ease-out 100ms; + outline: none; + box-shadow: #000 0px 2px 4px; + cursor: pointer; +} .tableWrapper .tableControls .tableToolbox, .tableWrapper .tableControls .tableColorPickerToolbox { border: 1px solid rgba(var(--color-border-300)); diff --git a/web/types/app.d.ts b/web/types/app.d.ts index 2b03f6975b9..c762fb76fc5 100644 --- a/web/types/app.d.ts +++ b/web/types/app.d.ts @@ -1,3 +1,12 @@ export type NextPageWithLayout

    = NextPage & { getLayout?: (page: ReactElement) => ReactNode; }; + +export interface IAppConfig { + email_password_login: boolean; + google_client_id: string | null; + github_app_name: string | null; + github_client_id: string | null; + magic_login: boolean; + slack_client_id: string | null; +} diff --git a/web/types/cycles.d.ts b/web/types/cycles.d.ts index e97aa21339c..c3c5248aabb 100644 --- a/web/types/cycles.d.ts +++ b/web/types/cycles.d.ts @@ -1,13 +1,4 @@ -import type { - IUser, - IIssue, - IProject, - IProjectLite, - IWorkspace, - IWorkspaceLite, - IIssueFilterOptions, - IUserLite, -} from "types"; +import type { IUser, IIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "types"; export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; @@ -20,7 +11,7 @@ export interface ICycle { created_at: Date; created_by: string; description: string; - distribution: { + distribution?: { assignees: TAssigneesDistribution[]; completion_chart: TCompletionChartDistribution; labels: TLabelsDistribution[]; diff --git a/yarn.lock b/yarn.lock index d7fd466263c..f25d1cf7d56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1343,10 +1343,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/eslintrc@^2.0.1", "@eslint/eslintrc@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" - integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== +"@eslint/eslintrc@^2.0.1", "@eslint/eslintrc@^2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.3.tgz#797470a75fe0fbd5a53350ee715e85e87baff22d" + integrity sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1363,10 +1363,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== -"@eslint/js@8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.52.0.tgz#78fe5f117840f69dc4a353adf9b9cd926353378c" - integrity sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA== +"@eslint/js@8.53.0": + version "8.53.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.53.0.tgz#bea56f2ed2b5baea164348ff4d5a879f6f81f20d" + integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w== "@floating-ui/core@^1.4.2": version "1.5.0" @@ -2758,13 +2758,6 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d" integrity sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g== -"@types/react-beautiful-dnd@^13.1.2": - version "13.1.6" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.6.tgz#a616443903bfc370fee298b0144dbce7234d5561" - integrity sha512-FXAuaa52ux7HWQDumi3MFSAAsW8OKfDImy1pSZPKe85nV9mZ1f4spVzW0a2boYvkIhphjbWUi5EwUiRG8Rq/Qg== - dependencies: - "@types/react" "*" - "@types/react-color@^3.0.6", "@types/react-color@^3.0.9": version "3.0.9" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.9.tgz#8dbb0d798f2979c3d7e2e26dd46321e80da950b4" @@ -2790,13 +2783,6 @@ dependencies: "@types/react" "*" -"@types/react-dom@18.0.6": - version "18.0.6" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1" - integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA== - dependencies: - "@types/react" "*" - "@types/react-dom@18.2.0": version "18.2.0" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.0.tgz#374f28074bb117f56f58c4f3f71753bebb545156" @@ -2804,6 +2790,13 @@ dependencies: "@types/react" "*" +"@types/react-dom@^18.2.14": + version "18.2.14" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.14.tgz#c01ba40e5bb57fc1dc41569bb3ccdb19eab1c539" + integrity sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.4.8": version "4.4.8" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.8.tgz#46f87d80512959cac793ecc610a93d80ef241ccf" @@ -2811,7 +2804,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.0.15", "@types/react@18.0.28", "@types/react@18.2.0", "@types/react@^18.2.5": +"@types/react@*", "@types/react@18.0.28", "@types/react@18.2.0", "@types/react@^18.2.35", "@types/react@^18.2.5": version "18.2.0" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.0.tgz#15cda145354accfc09a18d2f2305f9fc099ada21" integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA== @@ -3185,9 +3178,9 @@ astral-regex@^2.0.0: integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@^3.2.3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynciterator.prototype@^1.0.0: version "1.0.0" @@ -3428,9 +3421,9 @@ camelcase-css@^2.0.1: integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: - version "1.0.30001559" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz#95a982440d3d314c471db68d02664fb7536c5a30" - integrity sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA== + version "1.0.30001561" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz#752f21f56f96f1b1a52e97aae98c57c562d5d9da" + integrity sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw== capital-case@^1.0.4: version "1.0.4" @@ -4027,9 +4020,9 @@ ejs@^3.1.6: jake "^10.8.5" electron-to-chromium@^1.4.535: - version "1.4.575" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.575.tgz#7c0b87eb2c6214a993699792abd704de41255c39" - integrity sha512-kY2BGyvgAHiX899oF6xLXSIf99bAvvdPhDoJwG77nxCSyWYuRH6e9a9a3gpXBvCs6lj4dQZJkfnW2hdKWHEISg== + version "1.4.576" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz#0c6940fdc0d60f7e34bd742b29d8fa847c9294d1" + integrity sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA== emoji-regex@^8.0.0: version "8.0.0" @@ -4735,14 +4728,14 @@ eslint@^7.23.0, eslint@^7.32.0: v8-compile-cache "^2.0.3" eslint@^8.31.0: - version "8.52.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.52.0.tgz#d0cd4a1fac06427a61ef9242b9353f36ea7062fc" - integrity sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg== + version "8.53.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.53.0.tgz#14f2c8244298fcae1f46945459577413ba2697ce" + integrity sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.2" - "@eslint/js" "8.52.0" + "@eslint/eslintrc" "^2.1.3" + "@eslint/js" "8.53.0" "@humanwhocodes/config-array" "^0.11.13" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -6505,9 +6498,9 @@ mz@^2.7.0: thenify-all "^1.0.0" nanoid@^3.3.4, nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== napi-build-utils@^1.0.1: version "1.0.2" @@ -7332,9 +7325,9 @@ react-fast-compare@^3.0.1: integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== react-hook-form@^7.38.0: - version "7.47.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.47.0.tgz#a42f07266bd297ddf1f914f08f4b5f9783262f31" - integrity sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg== + version "7.48.2" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.48.2.tgz#01150354d2be61412ff56a030b62a119283b9935" + integrity sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A== react-is@^16.13.1, react-is@^16.7.0: version "16.13.1"