diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 00000000000..85de1a5e8d4 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,17 @@ +version = 1 + +[[analyzers]] +name = "shell" + +[[analyzers]] +name = "javascript" + + [analyzers.meta] + plugins = ["react"] + environment = ["nodejs"] + +[[analyzers]] +name = "python" + + [analyzers.meta] + runtime_version = "3.x.x" \ No newline at end of file diff --git a/.env.example b/.env.example index 1d95c56a067..082aa753b80 100644 --- a/.env.example +++ b/.env.example @@ -1,36 +1,3 @@ -# Frontend -# Extra image domains that need to be added for Next Image -NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= -# Google Client ID for Google OAuth -NEXT_PUBLIC_GOOGLE_CLIENTID="" -# Github ID for Github OAuth -NEXT_PUBLIC_GITHUB_ID="" -# Github App Name for GitHub Integration -NEXT_PUBLIC_GITHUB_APP_NAME="" -# Sentry DSN for error monitoring -NEXT_PUBLIC_SENTRY_DSN="" -# Enable/Disable OAUTH - default 0 for selfhosted instance -NEXT_PUBLIC_ENABLE_OAUTH=0 -# Enable/Disable sentry -NEXT_PUBLIC_ENABLE_SENTRY=0 -# Enable/Disable session recording -NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0 -# Enable/Disable event tracking -NEXT_PUBLIC_TRACK_EVENTS=0 -# Slack for Slack Integration -NEXT_PUBLIC_SLACK_CLIENT_ID="" -# For Telemetry, set it to "app.plane.so" -NEXT_PUBLIC_PLAUSIBLE_DOMAIN="" -# public boards deploy url -NEXT_PUBLIC_DEPLOY_URL="" - -# Backend -# Debug value for api server use it as 0 for production use -DEBUG=0 - -# Error logs -SENTRY_DSN="" - # Database Settings PGUSER="plane" PGPASSWORD="plane" @@ -43,15 +10,6 @@ REDIS_HOST="plane-redis" REDIS_PORT="6379" REDIS_URL="redis://${REDIS_HOST}:6379/" -# Email Settings -EMAIL_HOST="" -EMAIL_HOST_USER="" -EMAIL_HOST_PASSWORD="" -EMAIL_PORT=587 -EMAIL_FROM="Team Plane " -EMAIL_USE_TLS="1" -EMAIL_USE_SSL="0" - # AWS Settings AWS_REGION="" AWS_ACCESS_KEY_ID="access-key" @@ -67,9 +25,6 @@ OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint OPENAI_API_KEY="sk-" # add your openai key here GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access -# Github -GITHUB_CLIENT_SECRET="" # For fetching release notes - # Settings related to Docker DOCKERIZED=1 # set to 1 If using the pre-configured minio setup @@ -78,10 +33,3 @@ USE_MINIO=1 # Nginx Configuration NGINX_PORT=80 -# Default Creds -DEFAULT_EMAIL="captain@plane.so" -DEFAULT_PASSWORD="password123" - -# SignUps -ENABLE_SIGNUP="1" -# Auto generated and Required that will be generated from setup.sh diff --git a/.eslintrc.js b/.eslintrc.js index 463c86901c0..c229c095269 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { extends: ["custom"], settings: { next: { - rootDir: ["apps/*"], + rootDir: ["web/", "space/"], }, }, }; diff --git a/.github/workflows/Build_Test_Pull_Request.yml b/.github/workflows/Build_Test_Pull_Request.yml deleted file mode 100644 index 0dbca646a88..00000000000 --- a/.github/workflows/Build_Test_Pull_Request.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Build Pull Request Contents - -on: - pull_request: - types: ["opened", "synchronize"] - -jobs: - build-pull-request-contents: - name: Build Pull Request Contents - runs-on: ubuntu-20.04 - permissions: - pull-requests: read - - steps: - - name: Checkout Repository to Actions - uses: actions/checkout@v3.3.0 - - - name: Setup Node.js 18.x - uses: actions/setup-node@v2 - with: - node-version: 18.x - cache: 'yarn' - - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@v38 - with: - files_yaml: | - apiserver: - - apiserver/** - web: - - apps/app/** - deploy: - - apps/space/** - - - name: Setup .npmrc for repository - run: | - echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc - - - name: Build Plane's Main App - if: steps.changed-files.outputs.web_any_changed == 'true' - run: | - mv ./.npmrc ./apps/app - cd apps/app - yarn - yarn build - - - name: Build Plane's Deploy App - if: steps.changed-files.outputs.deploy_any_changed == 'true' - run: | - cd apps/space - yarn - yarn build - - diff --git a/.github/workflows/Update_Docker_Images.yml b/.github/workflows/Update_Docker_Images.yml deleted file mode 100644 index 8e27e098f98..00000000000 --- a/.github/workflows/Update_Docker_Images.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Update Docker Images for Plane on Release - -on: - release: - types: [released] - -jobs: - build_push_backend: - name: Build and Push Api Server Docker Image - runs-on: ubuntu-20.04 - - steps: - - name: Check out the repo - uses: actions/checkout@v3.3.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2.5.0 - - - name: Login to Docker Hub - uses: docker/login-action@v2.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Setup .npmrc for repository - run: | - echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc - - - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release - id: metaFrontend - uses: docker/metadata-action@v4.3.0 - with: - images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend - tags: | - type=ref,event=tag - - - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release - id: metaBackend - uses: docker/metadata-action@v4.3.0 - with: - images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend - tags: | - type=ref,event=tag - - - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release - id: metaDeploy - uses: docker/metadata-action@v4.3.0 - with: - images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy - tags: | - type=ref,event=tag - - - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release - id: metaProxy - uses: docker/metadata-action@v4.3.0 - with: - images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy - tags: | - type=ref,event=tag - - - name: Build and Push Frontend to Docker Container Registry - uses: docker/build-push-action@v4.0.0 - with: - context: . - file: ./apps/app/Dockerfile.web - platforms: linux/amd64 - tags: ${{ steps.metaFrontend.outputs.tags }} - push: true - env: - DOCKER_BUILDKIT: 1 - DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and Push Backend to Docker Hub - uses: docker/build-push-action@v4.0.0 - with: - context: ./apiserver - file: ./apiserver/Dockerfile.api - platforms: linux/amd64 - push: true - tags: ${{ steps.metaBackend.outputs.tags }} - env: - DOCKER_BUILDKIT: 1 - DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and Push Plane-Deploy to Docker Hub - uses: docker/build-push-action@v4.0.0 - with: - context: . - file: ./apps/space/Dockerfile.space - platforms: linux/amd64 - push: true - tags: ${{ steps.metaDeploy.outputs.tags }} - env: - DOCKER_BUILDKIT: 1 - DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and Push Plane-Proxy to Docker Hub - uses: docker/build-push-action@v4.0.0 - with: - context: ./nginx - file: ./nginx/Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ steps.metaProxy.outputs.tags }} - env: - DOCKER_BUILDKIT: 1 - DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml new file mode 100644 index 00000000000..26b8addd23b --- /dev/null +++ b/.github/workflows/build-branch.yml @@ -0,0 +1,205 @@ + +name: Docker Branch Build + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Dev/QA Builds' + +env: + gh_branch: ${{ github.ref_name }} + img_tag: latest + +jobs: + branch_build_and_push: + 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 + + - uses: ASzc/change-string-case-action@v2 + id: gh_branch_upper_lower + with: + string: ${{ env.gh_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 }} + DOCKET_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 }} + DOCKET_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 }} + DOCKET_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 }} + DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/build-test-pull-request.yml b/.github/workflows/build-test-pull-request.yml new file mode 100644 index 00000000000..c74975f48ef --- /dev/null +++ b/.github/workflows/build-test-pull-request.yml @@ -0,0 +1,48 @@ +name: Build Pull Request Contents + +on: + pull_request: + types: ["opened", "synchronize"] + +jobs: + build-pull-request-contents: + name: Build Pull Request Contents + runs-on: ubuntu-20.04 + permissions: + pull-requests: read + + steps: + - name: Checkout Repository to Actions + uses: actions/checkout@v3.3.0 + + - name: Setup Node.js 18.x + uses: actions/setup-node@v2 + with: + node-version: 18.x + cache: 'yarn' + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v38 + with: + files_yaml: | + apiserver: + - apiserver/** + web: + - web/** + deploy: + - space/** + + - name: Build Plane's Main App + if: steps.changed-files.outputs.web_any_changed == 'true' + run: | + yarn + yarn build --filter=web + + - name: Build Plane's Deploy App + if: steps.changed-files.outputs.deploy_any_changed == 'true' + run: | + yarn + yarn build --filter=space + + diff --git a/.github/workflows/create-sync-pr.yml b/.github/workflows/create-sync-pr.yml new file mode 100644 index 00000000000..c8e27f32216 --- /dev/null +++ b/.github/workflows/create-sync-pr.yml @@ -0,0 +1,79 @@ +name: Create PR in Plane EE Repository to sync the changes + +on: + pull_request: + branches: + - master + types: + - closed + +jobs: + create_pr: + # Only run the job when a PR is merged + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + steps: + - name: Check SOURCE_REPO + id: check_repo + env: + SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }} + run: | + echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)" + + - name: Checkout Code + if: steps.check_repo.outputs.is_correct_repo == 'true' + uses: actions/checkout@v2 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Set up Branch Name + if: steps.check_repo.outputs.is_correct_repo == 'true' + run: | + echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV + + - name: Setup GH CLI + if: steps.check_repo.outputs.is_correct_repo == 'true' + run: | + type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + sudo apt update + sudo apt install gh -y + + - name: Create Pull Request + if: steps.check_repo.outputs.is_correct_repo == 'true' + env: + GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} + run: | + TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}" + TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}" + SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" + + git checkout $SOURCE_BRANCH + git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git" + git push target $SOURCE_BRANCH:$SOURCE_BRANCH + + PR_TITLE="${{ github.event.pull_request.title }}" + PR_BODY="${{ github.event.pull_request.body }}" + + # Remove double quotes + PR_TITLE_CLEANED="${PR_TITLE//\"/}" + PR_BODY_CLEANED="${PR_BODY//\"/}" + + # Construct PR_BODY_CONTENT using a here-document + PR_BODY_CONTENT=$(cat <> ./web/.env +``` + +```bash +echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env +``` + +4. Run Docker compose up + +```bash +docker compose up -d +``` + +5. Install dependencies + +```bash +yarn install +``` + +6. Run the web app in development mode + +```bash +yarn dev +``` + ## Missing a Feature? If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository. @@ -39,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 @@ -48,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/Dockerfile b/Dockerfile index 1b059b5e043..1a71801f63b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,7 @@ ENV DJANGO_SETTINGS_MODULE plane.settings.production ENV DOCKERIZED 1 WORKDIR /code - +RUN sed -i 's/dl-cdn.alpinelinux.org/mirror.tuna.tsinghua.edu.cn/g' /etc/apk/repositories RUN apk --no-cache add \ "libpq~=15" \ "libxslt~=1.1" \ diff --git a/ENV_SETUP.md b/ENV_SETUP.md new file mode 100644 index 00000000000..6796c3db6b1 --- /dev/null +++ b/ENV_SETUP.md @@ -0,0 +1,134 @@ +# Environment Variables +​ +Environment variables are distributed in various files. Please refer them carefully. + +## {PROJECT_FOLDER}/.env +File is available in the project root folder​ + +``` +# Database Settings +PGUSER="plane" +PGPASSWORD="plane" +PGHOST="plane-db" +PGDATABASE="plane" +DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE} +​ +# Redis Settings +REDIS_HOST="plane-redis" +REDIS_PORT="6379" +REDIS_URL="redis://${REDIS_HOST}:6379/" +​ +# AWS Settings +AWS_REGION="" +AWS_ACCESS_KEY_ID="access-key" +AWS_SECRET_ACCESS_KEY="secret-key" +AWS_S3_ENDPOINT_URL="http://plane-minio:9000" +# Changing this requires change in the nginx.conf for uploads if using minio setup +AWS_S3_BUCKET_NAME="uploads" +# Maximum file upload limit +FILE_SIZE_LIMIT=5242880 +​ +# GPT settings +OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint +OPENAI_API_KEY="sk-" # add your openai key here +GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access +​ +# Settings related to Docker +DOCKERIZED=1 +# set to 1 If using the pre-configured minio setup +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 +DEBUG=0 +DJANGO_SETTINGS_MODULE="plane.settings.selfhosted" +​ +# Error logs +SENTRY_DSN="" +​ +# Database Settings +PGUSER="plane" +PGPASSWORD="plane" +PGHOST="plane-db" +PGDATABASE="plane" +DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE} +​ +# Redis Settings +REDIS_HOST="plane-redis" +REDIS_PORT="6379" +REDIS_URL="redis://${REDIS_HOST}:6379/" +​ +# Email Settings +EMAIL_HOST="" +EMAIL_HOST_USER="" +EMAIL_HOST_PASSWORD="" +EMAIL_PORT=587 +EMAIL_FROM="Team Plane " +EMAIL_USE_TLS="1" +EMAIL_USE_SSL="0" +​ +# AWS Settings +AWS_REGION="" +AWS_ACCESS_KEY_ID="access-key" +AWS_SECRET_ACCESS_KEY="secret-key" +AWS_S3_ENDPOINT_URL="http://plane-minio:9000" +# Changing this requires change in the nginx.conf for uploads if using minio setup +AWS_S3_BUCKET_NAME="uploads" +# Maximum file upload limit +FILE_SIZE_LIMIT=5242880 +​ +# GPT settings +OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint +OPENAI_API_KEY="sk-" # add your openai key here +GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access +​ +# Github +GITHUB_CLIENT_SECRET="" # For fetching release notes +​ +# Settings related to Docker +DOCKERIZED=1 +# set to 1 If using the pre-configured minio setup +USE_MINIO=1 +​ +# Nginx Configuration +NGINX_PORT=80 +​ +# Default Creds +DEFAULT_EMAIL="captain@plane.so" +DEFAULT_PASSWORD="password123" +​ +# SignUps +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. +- The Tiptap pro-extension dependency has been removed, eliminating the need for Tiptap API keys. +- The image name for Plane deployment has been changed to plane-space. diff --git a/README.md b/README.md index 2bc2764f3bc..53679943ba3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

Plane

-

Open-source, self-hosted project planning tool

+

Flexible, extensible open-source project management

@@ -35,61 +35,51 @@ Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘‍♀️. - > Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases. The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting). +## ⚡️ Contributors Quick Start -## ⚡️ Quick start with Docker Compose +### Prerequisite -### Docker Compose Setup +Development system must have docker engine installed and running. -- Clone the repository +### Steps -```bash -git clone https://github.com/makeplane/plane -cd plane -chmod +x setup.sh -``` +Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute -- Run setup.sh +1. Clone the code locally using `git clone https://github.com/makeplane/plane.git` +1. Switch to the code folder `cd plane` +1. Create your feature or fix branch you plan to work on using `git checkout -b ` +1. Open terminal and run `./setup.sh` +1. Open the code on VSCode or similar equivalent IDE +1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system +1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d` ```bash -./setup.sh http://localhost +./setup.sh ``` -> If running in a cloud env replace localhost with public facing IP address of the VM +You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload) -- Setup Tiptap Pro +Thats it! - Visit [Tiptap Pro](https://collab.tiptap.dev/pro-extensions) and signup (it is free). +## 🍙 Self Hosting - Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro. - -``` -@tiptap-pro:registry=https://registry.tiptap.dev/ -//registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN -``` -- Run Docker compose up - -```bash -docker compose up -d -``` - -You can use the default email and password for your first login `captain@plane.so` and `password123`. +For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page ## 🚀 Features -* **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking. -* **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents. -* **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you. -* **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features. -* **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress. -* **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks. -* **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues. -* **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location. -* **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration. +- **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking. +- **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents. +- **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you. +- **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features. +- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress. +- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks. +- **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues. +- **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location. +- **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration. ## 📸 Screenshots @@ -150,7 +140,6 @@ docker compose up -d

- ## 📚Documentation For full documentation, visit [docs.plane.so](https://docs.plane.so/) diff --git a/apiserver/.env.example b/apiserver/.env.example new file mode 100644 index 00000000000..8193b5e7716 --- /dev/null +++ b/apiserver/.env.example @@ -0,0 +1,72 @@ +# Backend +# Debug value for api server use it as 0 for production use +DEBUG=0 +DJANGO_SETTINGS_MODULE="plane.settings.production" + +# Error logs +SENTRY_DSN="" + +# Database Settings +PGUSER="plane" +PGPASSWORD="plane" +PGHOST="plane-db" +PGDATABASE="plane" +DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE} + +# Redis Settings +REDIS_HOST="plane-redis" +REDIS_PORT="6379" +REDIS_URL="redis://${REDIS_HOST}:6379/" + +# Email Settings +EMAIL_HOST="" +EMAIL_HOST_USER="" +EMAIL_HOST_PASSWORD="" +EMAIL_PORT=587 +EMAIL_FROM="Team Plane " +EMAIL_USE_TLS="1" +EMAIL_USE_SSL="0" + +# AWS Settings +AWS_REGION="" +AWS_ACCESS_KEY_ID="access-key" +AWS_SECRET_ACCESS_KEY="secret-key" +AWS_S3_ENDPOINT_URL="http://plane-minio:9000" +# Changing this requires change in the nginx.conf for uploads if using minio setup +AWS_S3_BUCKET_NAME="uploads" +# Maximum file upload limit +FILE_SIZE_LIMIT=5242880 + +# GPT settings +OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint +OPENAI_API_KEY="sk-" # add your openai key here +GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access + +# Github +GITHUB_CLIENT_SECRET="" # For fetching release notes + +# Settings related to Docker +DOCKERIZED=1 +# set to 1 If using the pre-configured minio setup +USE_MINIO=1 + +# Nginx Configuration +NGINX_PORT=80 + +# Default Creds +DEFAULT_EMAIL="captain@plane.so" +DEFAULT_PASSWORD="password123" + +# SignUps +ENABLE_SIGNUP="1" + + +# Enable Email/Password Signup +ENABLE_EMAIL_PASSWORD="1" + +# Enable Magic link Login +ENABLE_MAGIC_LINK_LOGIN="0" + +# Email redirections and minio domain settings +WEB_URL="http://localhost" + diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 7da5f9ddaf1..cd1ee966768 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -8,7 +8,6 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 WORKDIR /code RUN sed -i 's/dl-cdn.alpinelinux.org/mirror.tuna.tsinghua.edu.cn/g' /etc/apk/repositories - RUN apk --no-cache add \ "libpq~=15" \ "libxslt~=1.1" \ diff --git a/apiserver/Dockerfile.dev b/apiserver/Dockerfile.dev new file mode 100644 index 00000000000..cdfd2d50dcb --- /dev/null +++ b/apiserver/Dockerfile.dev @@ -0,0 +1,51 @@ +FROM python:3.11.1-alpine3.17 AS backend + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirror.tuna.tsinghua.edu.cn/g' /etc/apk/repositories +RUN apk --no-cache add \ + "bash~=5.2" \ + "libpq~=15" \ + "libxslt~=1.1" \ + "nodejs-current~=19" \ + "xmlsec~=1.2" \ + "libffi-dev" \ + "bash~=5.2" \ + "g++~=12.2" \ + "gcc~=12.2" \ + "cargo~=1.64" \ + "git~=2" \ + "make~=4.3" \ + "postgresql13-dev~=13" \ + "libc-dev" \ + "linux-headers" + +WORKDIR /code + +COPY requirements.txt ./requirements.txt +ADD requirements ./requirements + +RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn -r requirements.txt --compile --no-cache-dir +RUN addgroup -S plane && \ + adduser -S captain -G plane + +RUN chown captain.plane /code + +USER captain + +# Add in Django deps and generate Django's static files + +USER root + +# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat +RUN chmod -R 777 /code + +USER captain + +# Expose container port and run entry point script +EXPOSE 8000 + +# CMD [ "./bin/takeoff" ] + diff --git a/apiserver/bin/user_script.py b/apiserver/bin/user_script.py index e115b20b8c0..a356f2ec92b 100644 --- a/apiserver/bin/user_script.py +++ b/apiserver/bin/user_script.py @@ -1,4 +1,4 @@ -import os, sys, random, string +import os, sys import uuid sys.path.append("/code") diff --git a/apiserver/gunicorn.config.py b/apiserver/gunicorn.config.py index 67205b5ec94..51c2a548871 100644 --- a/apiserver/gunicorn.config.py +++ b/apiserver/gunicorn.config.py @@ -3,4 +3,4 @@ def post_fork(server, worker): patch_psycopg() - worker.log.info("Made Psycopg2 Green") \ No newline at end of file + worker.log.info("Made Psycopg2 Green") diff --git a/apiserver/plane/api/permissions/project.py b/apiserver/plane/api/permissions/project.py index e4e3e0f9bc3..4f907dbd6fa 100644 --- a/apiserver/plane/api/permissions/project.py +++ b/apiserver/plane/api/permissions/project.py @@ -101,4 +101,4 @@ def has_permission(self, request, view): workspace__slug=view.workspace_slug, member=request.user, project_id=view.project_id, - ).exists() \ No newline at end of file + ).exists() diff --git a/apiserver/plane/api/permissions/workspace.py b/apiserver/plane/api/permissions/workspace.py index d01b545ee18..66e8366146c 100644 --- a/apiserver/plane/api/permissions/workspace.py +++ b/apiserver/plane/api/permissions/workspace.py @@ -58,8 +58,17 @@ def has_permission(self, request, view): if request.user.is_anonymous: return False + ## Safe Methods -> Handle the filtering logic in queryset + if request.method in SAFE_METHODS: + return WorkspaceMember.objects.filter( + workspace__slug=view.workspace_slug, + member=request.user, + ).exists() + return WorkspaceMember.objects.filter( - member=request.user, workspace__slug=view.workspace_slug + member=request.user, + workspace__slug=view.workspace_slug, + role__in=[Owner, Admin], ).exists() diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 2dc910cafcd..f1a7de3b81c 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -1,5 +1,13 @@ from .base import BaseSerializer -from .user import UserSerializer, UserLiteSerializer, ChangePasswordSerializer, ResetPasswordSerializer, UserAdminLiteSerializer +from .user import ( + UserSerializer, + UserLiteSerializer, + ChangePasswordSerializer, + ResetPasswordSerializer, + UserAdminLiteSerializer, + UserMeSerializer, + UserMeSettingsSerializer, +) from .workspace import ( WorkSpaceSerializer, WorkSpaceMemberSerializer, @@ -8,9 +16,11 @@ WorkspaceLiteSerializer, WorkspaceThemeSerializer, WorkspaceMemberAdminSerializer, + WorkspaceMemberMeSerializer, ) from .project import ( ProjectSerializer, + ProjectListSerializer, ProjectDetailSerializer, ProjectMemberSerializer, ProjectMemberInviteSerializer, @@ -20,19 +30,22 @@ ProjectMemberLiteSerializer, ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, - ProjectPublicMemberSerializer + ProjectPublicMemberSerializer, ) from .state import StateSerializer, StateLiteSerializer -from .view import IssueViewSerializer, IssueViewFavoriteSerializer -from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer +from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer +from .cycle import ( + CycleSerializer, + CycleIssueSerializer, + CycleFavoriteSerializer, + CycleWriteSerializer, +) from .asset import FileAssetSerializer from .issue import ( IssueCreateSerializer, IssueActivitySerializer, IssueCommentSerializer, IssuePropertySerializer, - BlockerIssueSerializer, - BlockedIssueSerializer, IssueAssigneeSerializer, LabelSerializer, IssueSerializer, @@ -45,6 +58,8 @@ IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssueRelationSerializer, + RelatedIssueSerializer, IssuePublicSerializer, ) diff --git a/apiserver/plane/api/serializers/analytic.py b/apiserver/plane/api/serializers/analytic.py index 5f35e111787..9f3ee6d0a24 100644 --- a/apiserver/plane/api/serializers/analytic.py +++ b/apiserver/plane/api/serializers/analytic.py @@ -17,7 +17,7 @@ def create(self, validated_data): if bool(query_params): validated_data["query"] = issue_filters(query_params, "POST") else: - validated_data["query"] = dict() + validated_data["query"] = {} return AnalyticView.objects.create(**validated_data) def update(self, instance, validated_data): @@ -25,6 +25,6 @@ def update(self, instance, validated_data): if bool(query_params): validated_data["query"] = issue_filters(query_params, "POST") else: - validated_data["query"] = dict() + validated_data["query"] = {} validated_data["query"] = issue_filters(query_params, "PATCH") return super().update(instance, validated_data) diff --git a/apiserver/plane/api/serializers/base.py b/apiserver/plane/api/serializers/base.py index 0c6bba46823..89c9725d951 100644 --- a/apiserver/plane/api/serializers/base.py +++ b/apiserver/plane/api/serializers/base.py @@ -3,3 +3,56 @@ class BaseSerializer(serializers.ModelSerializer): id = serializers.PrimaryKeyRelatedField(read_only=True) + +class DynamicBaseSerializer(BaseSerializer): + + def __init__(self, *args, **kwargs): + # If 'fields' is provided in the arguments, remove it and store it separately. + # This is done so as not to pass this custom argument up to the superclass. + fields = kwargs.pop("fields", None) + + # Call the initialization of the superclass. + super().__init__(*args, **kwargs) + + # If 'fields' was provided, filter the fields of the serializer accordingly. + if fields is not None: + self.fields = self._filter_fields(fields) + + def _filter_fields(self, fields): + """ + Adjust the serializer's fields based on the provided 'fields' list. + + :param fields: List or dictionary specifying which fields to include in the serializer. + :return: The updated fields for the serializer. + """ + # Check each field_name in the provided fields. + for field_name in fields: + # If the field is a dictionary (indicating nested fields), + # loop through its keys and values. + if isinstance(field_name, dict): + for key, value in field_name.items(): + # If the value of this nested field is a list, + # perform a recursive filter on it. + if isinstance(value, list): + self._filter_fields(self.fields[key], value) + + # Create a list to store allowed fields. + allowed = [] + for item in fields: + # If the item is a string, it directly represents a field's name. + if isinstance(item, str): + allowed.append(item) + # If the item is a dictionary, it represents a nested field. + # Add the key of this dictionary to the allowed list. + elif isinstance(item, dict): + allowed.append(list(item.keys())[0]) + + # Convert the current serializer's fields and the allowed fields to sets. + existing = set(self.fields) + allowed = set(allowed) + + # Remove fields from the serializer that aren't in the 'allowed' list. + for field_name in (existing - allowed): + self.fields.pop(field_name) + + return self.fields diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index 66436803333..104a3dd067a 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -1,6 +1,3 @@ -# Django imports -from django.db.models.functions import TruncDate - # Third party imports from rest_framework import serializers @@ -12,10 +9,14 @@ from .project import ProjectLiteSerializer from plane.db.models import Cycle, CycleIssue, CycleFavorite -class CycleWriteSerializer(BaseSerializer): +class CycleWriteSerializer(BaseSerializer): def validate(self, data): - if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None): + if ( + data.get("start_date", None) is not None + and data.get("end_date", None) is not None + and data.get("start_date", None) > data.get("end_date", None) + ): raise serializers.ValidationError("Start date cannot exceed end date") return data @@ -34,7 +35,6 @@ class CycleSerializer(BaseSerializer): unstarted_issues = serializers.IntegerField(read_only=True) backlog_issues = serializers.IntegerField(read_only=True) assignees = serializers.SerializerMethodField(read_only=True) - labels = serializers.SerializerMethodField(read_only=True) total_estimates = serializers.IntegerField(read_only=True) completed_estimates = serializers.IntegerField(read_only=True) started_estimates = serializers.IntegerField(read_only=True) @@ -42,19 +42,24 @@ class CycleSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") def validate(self, data): - if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None): + if ( + data.get("start_date", None) is not None + and data.get("end_date", None) is not None + and data.get("start_date", None) > data.get("end_date", None) + ): raise serializers.ValidationError("Start date cannot exceed end date") return data - + def get_assignees(self, obj): members = [ { "avatar": assignee.avatar, - "first_name": assignee.first_name, "display_name": assignee.display_name, "id": assignee.id, } - for issue_cycle in obj.issue_cycle.all() + for issue_cycle in obj.issue_cycle.prefetch_related( + "issue__assignees" + ).all() for assignee in issue_cycle.issue.assignees.all() ] # Use a set comprehension to return only the unique objects @@ -64,24 +69,6 @@ def get_assignees(self, obj): unique_list = [dict(item) for item in unique_objects] return unique_list - - def get_labels(self, obj): - labels = [ - { - "name": label.name, - "color": label.color, - "id": label.id, - } - for issue_cycle in obj.issue_cycle.all() - for label in issue_cycle.issue.labels.all() - ] - # Use a set comprehension to return only the unique objects - unique_objects = {frozenset(item.items()) for item in labels} - - # Convert the set back to a list of dictionaries - unique_list = [dict(item) for item in unique_objects] - - return unique_list class Meta: model = Cycle diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/api/serializers/inbox.py index ae17b749bfa..f52a90660be 100644 --- a/apiserver/plane/api/serializers/inbox.py +++ b/apiserver/plane/api/serializers/inbox.py @@ -6,7 +6,6 @@ from .issue import IssueFlatSerializer, LabelLiteSerializer from .project import ProjectLiteSerializer from .state import StateLiteSerializer -from .project import ProjectLiteSerializer from .user import UserLiteSerializer from plane.db.models import Inbox, InboxIssue, Issue diff --git a/apiserver/plane/api/serializers/integration/__init__.py b/apiserver/plane/api/serializers/integration/__init__.py index 963fc295e27..112ff02d162 100644 --- a/apiserver/plane/api/serializers/integration/__init__.py +++ b/apiserver/plane/api/serializers/integration/__init__.py @@ -5,4 +5,4 @@ GithubIssueSyncSerializer, GithubCommentSyncSerializer, ) -from .slack import SlackProjectSyncSerializer \ No newline at end of file +from .slack import SlackProjectSyncSerializer diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 2a75b2f48fb..f061a0a1938 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -8,8 +8,7 @@ from .base import BaseSerializer from .user import UserLiteSerializer from .state import StateSerializer, StateLiteSerializer -from .user import UserLiteSerializer -from .project import ProjectSerializer, ProjectLiteSerializer +from .project import ProjectLiteSerializer from .workspace import WorkspaceLiteSerializer from plane.db.models import ( User, @@ -17,12 +16,10 @@ IssueActivity, IssueComment, IssueProperty, - IssueBlocker, IssueAssignee, IssueSubscriber, IssueLabel, Label, - IssueBlocker, CycleIssue, Cycle, Module, @@ -32,6 +29,7 @@ IssueReaction, CommentReaction, IssueVote, + IssueRelation, ) @@ -50,6 +48,7 @@ class Meta: "target_date", "sequence_id", "sort_order", + "is_draft", ] @@ -75,31 +74,18 @@ class IssueCreateSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") - assignees_list = serializers.ListField( + assignees = serializers.ListField( child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), write_only=True, required=False, ) - # List of issues that are blocking this issue - blockers_list = serializers.ListField( - child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()), - write_only=True, - required=False, - ) - labels_list = serializers.ListField( + labels = serializers.ListField( child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), write_only=True, required=False, ) - # List of issues that are blocked by this issue - blocks_list = serializers.ListField( - child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()), - write_only=True, - required=False, - ) - class Meta: model = Issue fields = "__all__" @@ -112,6 +98,12 @@ class Meta: "updated_at", ] + def to_representation(self, instance): + data = super().to_representation(instance) + data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()] + data['labels'] = [str(label.id) for label in instance.labels.all()] + return data + def validate(self, data): if ( data.get("start_date", None) is not None @@ -122,10 +114,8 @@ def validate(self, data): return data def create(self, validated_data): - blockers = validated_data.pop("blockers_list", None) - assignees = validated_data.pop("assignees_list", None) - labels = validated_data.pop("labels_list", None) - blocks = validated_data.pop("blocks_list", None) + assignees = validated_data.pop("assignees", None) + labels = validated_data.pop("labels", None) project_id = self.context["project_id"] workspace_id = self.context["workspace_id"] @@ -137,22 +127,6 @@ def create(self, validated_data): created_by_id = issue.created_by_id updated_by_id = issue.updated_by_id - if blockers is not None and len(blockers): - IssueBlocker.objects.bulk_create( - [ - IssueBlocker( - block=issue, - blocked_by=blocker, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for blocker in blockers - ], - batch_size=10, - ) - if assignees is not None and len(assignees): IssueAssignee.objects.bulk_create( [ @@ -196,29 +170,11 @@ def create(self, validated_data): batch_size=10, ) - if blocks is not None and len(blocks): - IssueBlocker.objects.bulk_create( - [ - IssueBlocker( - block=block, - blocked_by=issue, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for block in blocks - ], - batch_size=10, - ) - return issue def update(self, instance, validated_data): - blockers = validated_data.pop("blockers_list", None) - assignees = validated_data.pop("assignees_list", None) - labels = validated_data.pop("labels_list", None) - blocks = validated_data.pop("blocks_list", None) + assignees = validated_data.pop("assignees", None) + labels = validated_data.pop("labels", None) # Related models project_id = instance.project_id @@ -226,23 +182,6 @@ def update(self, instance, validated_data): created_by_id = instance.created_by_id updated_by_id = instance.updated_by_id - if blockers is not None: - IssueBlocker.objects.filter(block=instance).delete() - IssueBlocker.objects.bulk_create( - [ - IssueBlocker( - block=instance, - blocked_by=blocker, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for blocker in blockers - ], - batch_size=10, - ) - if assignees is not None: IssueAssignee.objects.filter(issue=instance).delete() IssueAssignee.objects.bulk_create( @@ -277,23 +216,6 @@ def update(self, instance, validated_data): batch_size=10, ) - if blocks is not None: - IssueBlocker.objects.filter(blocked_by=instance).delete() - IssueBlocker.objects.bulk_create( - [ - IssueBlocker( - block=block, - blocked_by=instance, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for block in blocks - ], - batch_size=10, - ) - # Time updation occues even when other related models are updated instance.updated_at = timezone.now() return super().update(instance, validated_data) @@ -309,25 +231,6 @@ class Meta: fields = "__all__" -class IssueCommentSerializer(BaseSerializer): - actor_detail = UserLiteSerializer(read_only=True, source="actor") - issue_detail = IssueFlatSerializer(read_only=True, source="issue") - project_detail = ProjectLiteSerializer(read_only=True, source="project") - workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") - - class Meta: - model = IssueComment - fields = "__all__" - read_only_fields = [ - "workspace", - "project", - "issue", - "created_by", - "updated_by", - "created_at", - "updated_at", - ] - class IssuePropertySerializer(BaseSerializer): class Meta: @@ -364,7 +267,6 @@ class Meta: class IssueLabelSerializer(BaseSerializer): - # label_details = LabelSerializer(read_only=True, source="label") class Meta: model = IssueLabel @@ -375,32 +277,39 @@ class Meta: ] -class BlockedIssueSerializer(BaseSerializer): - blocked_issue_detail = IssueProjectLiteSerializer(source="block", read_only=True) +class IssueRelationSerializer(BaseSerializer): + issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue") class Meta: - model = IssueBlocker + model = IssueRelation fields = [ - "blocked_issue_detail", - "blocked_by", - "block", + "issue_detail", + "relation_type", + "related_issue", + "issue", + "id" + ] + read_only_fields = [ + "workspace", + "project", ] - read_only_fields = fields - -class BlockerIssueSerializer(BaseSerializer): - blocker_issue_detail = IssueProjectLiteSerializer( - source="blocked_by", read_only=True - ) +class RelatedIssueSerializer(BaseSerializer): + issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue") class Meta: - model = IssueBlocker + model = IssueRelation fields = [ - "blocker_issue_detail", - "blocked_by", - "block", + "issue_detail", + "relation_type", + "related_issue", + "issue", + "id" + ] + read_only_fields = [ + "workspace", + "project", ] - read_only_fields = fields class IssueAssigneeSerializer(BaseSerializer): @@ -514,6 +423,9 @@ class Meta: class IssueReactionSerializer(BaseSerializer): + + actor_detail = UserLiteSerializer(read_only=True, source="actor") + class Meta: model = IssueReaction fields = "__all__" @@ -525,19 +437,6 @@ class Meta: ] -class IssueReactionLiteSerializer(BaseSerializer): - actor_detail = UserLiteSerializer(read_only=True, source="actor") - - class Meta: - model = IssueReaction - fields = [ - "id", - "reaction", - "issue", - "actor_detail", - ] - - class CommentReactionLiteSerializer(BaseSerializer): actor_detail = UserLiteSerializer(read_only=True, source="actor") @@ -559,9 +458,12 @@ class Meta: class IssueVoteSerializer(BaseSerializer): + + actor_detail = UserLiteSerializer(read_only=True, source="actor") + class Meta: model = IssueVote - fields = ["issue", "vote", "workspace_id", "project_id", "actor"] + fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"] read_only_fields = fields @@ -624,16 +526,14 @@ class IssueSerializer(BaseSerializer): parent_detail = IssueStateFlatSerializer(read_only=True, source="parent") label_details = LabelSerializer(read_only=True, source="labels", many=True) assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) - # List of issues blocked by this issue - blocked_issues = BlockedIssueSerializer(read_only=True, many=True) - # List of issues that block this issue - blocker_issues = BlockerIssueSerializer(read_only=True, many=True) + related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True) + issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True) issue_cycle = IssueCycleDetailSerializer(read_only=True) issue_module = IssueModuleDetailSerializer(read_only=True) issue_link = IssueLinkSerializer(read_only=True, many=True) issue_attachment = IssueAttachmentSerializer(read_only=True, many=True) sub_issues_count = serializers.IntegerField(read_only=True) - issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) + issue_reactions = IssueReactionSerializer(read_only=True, many=True) class Meta: model = Issue @@ -659,7 +559,7 @@ class IssueLiteSerializer(BaseSerializer): module_id = serializers.UUIDField(read_only=True) attachment_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True) - issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) + issue_reactions = IssueReactionSerializer(read_only=True, many=True) class Meta: model = Issue @@ -680,7 +580,8 @@ class Meta: class IssuePublicSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state") - issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) + reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions") + votes = IssueVoteSerializer(read_only=True, many=True) class Meta: model = Issue @@ -696,11 +597,13 @@ class Meta: "workspace", "priority", "target_date", - "issue_reactions", + "reactions", + "votes", ] read_only_fields = fields + class IssueSubscriberSerializer(BaseSerializer): class Meta: model = IssueSubscriber diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py index aaabd4ae071..48f773b0f81 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -4,9 +4,8 @@ # Module imports from .base import BaseSerializer from .user import UserLiteSerializer -from .project import ProjectSerializer, ProjectLiteSerializer +from .project import ProjectLiteSerializer from .workspace import WorkspaceLiteSerializer -from .issue import IssueStateSerializer from plane.db.models import ( User, @@ -19,7 +18,7 @@ class ModuleWriteSerializer(BaseSerializer): - members_list = serializers.ListField( + members = serializers.ListField( child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), write_only=True, required=False, @@ -39,6 +38,11 @@ class Meta: "created_at", "updated_at", ] + + def to_representation(self, instance): + data = super().to_representation(instance) + data['members'] = [str(member.id) for member in instance.members.all()] + return data def validate(self, data): if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None): @@ -46,7 +50,7 @@ def validate(self, data): return data def create(self, validated_data): - members = validated_data.pop("members_list", None) + members = validated_data.pop("members", None) project = self.context["project"] @@ -72,7 +76,7 @@ def create(self, validated_data): return module def update(self, instance, validated_data): - members = validated_data.pop("members_list", None) + members = validated_data.pop("members", None) if members is not None: ModuleMember.objects.filter(module=instance).delete() diff --git a/apiserver/plane/api/serializers/page.py b/apiserver/plane/api/serializers/page.py index 94f7836de18..abdf958cb10 100644 --- a/apiserver/plane/api/serializers/page.py +++ b/apiserver/plane/api/serializers/page.py @@ -33,7 +33,7 @@ class Meta: class PageSerializer(BaseSerializer): is_favorite = serializers.BooleanField(read_only=True) label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) - labels_list = serializers.ListField( + labels = serializers.ListField( child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), write_only=True, required=False, @@ -50,9 +50,13 @@ class Meta: "project", "owned_by", ] + def to_representation(self, instance): + data = super().to_representation(instance) + data['labels'] = [str(label.id) for label in instance.labels.all()] + return data def create(self, validated_data): - labels = validated_data.pop("labels_list", None) + labels = validated_data.pop("labels", None) project_id = self.context["project_id"] owned_by_id = self.context["owned_by_id"] page = Page.objects.create( @@ -77,7 +81,7 @@ def create(self, validated_data): return page def update(self, instance, validated_data): - labels = validated_data.pop("labels_list", None) + labels = validated_data.pop("labels", None) if labels is not None: PageLabel.objects.filter(page=instance).delete() PageLabel.objects.bulk_create( diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 49d986cae0b..36fa6ecca7c 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -1,11 +1,8 @@ -# Django imports -from django.db import IntegrityError - # Third party imports from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer from plane.db.models import ( @@ -94,8 +91,33 @@ class Meta: read_only_fields = fields +class ProjectListSerializer(DynamicBaseSerializer): + is_favorite = serializers.BooleanField(read_only=True) + total_members = serializers.IntegerField(read_only=True) + total_cycles = serializers.IntegerField(read_only=True) + total_modules = serializers.IntegerField(read_only=True) + is_member = serializers.BooleanField(read_only=True) + sort_order = serializers.FloatField(read_only=True) + member_role = serializers.IntegerField(read_only=True) + is_deployed = serializers.BooleanField(read_only=True) + members = serializers.SerializerMethodField() + + def get_members(self, obj): + project_members = ProjectMember.objects.filter(project_id=obj.id).values( + "id", + "member_id", + "member__display_name", + "member__avatar", + ) + return project_members + + class Meta: + model = Project + fields = "__all__" + + class ProjectDetailSerializer(BaseSerializer): - workspace = WorkSpaceSerializer(read_only=True) + # workspace = WorkSpaceSerializer(read_only=True) default_assignee = UserLiteSerializer(read_only=True) project_lead = UserLiteSerializer(read_only=True) is_favorite = serializers.BooleanField(read_only=True) @@ -148,8 +170,6 @@ class Meta: class ProjectFavoriteSerializer(BaseSerializer): - project_detail = ProjectLiteSerializer(source="project", read_only=True) - class Meta: model = ProjectFavorite fields = "__all__" @@ -178,12 +198,12 @@ class Meta: fields = "__all__" read_only_fields = [ "workspace", - "project", "anchor", + "project", + "anchor", ] class ProjectPublicMemberSerializer(BaseSerializer): - class Meta: model = ProjectPublicMember fields = "__all__" diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index dcb00c6cbfe..b8f9dedd43d 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -3,7 +3,7 @@ # Module import from .base import BaseSerializer -from plane.db.models import User +from plane.db.models import User, Workspace, WorkspaceMemberInvite class UserSerializer(BaseSerializer): @@ -33,6 +33,81 @@ def get_is_onboarded(self, obj): return bool(obj.first_name) or bool(obj.last_name) +class UserMeSerializer(BaseSerializer): + class Meta: + model = User + fields = [ + "id", + "avatar", + "cover_image", + "date_joined", + "display_name", + "email", + "first_name", + "last_name", + "is_active", + "is_bot", + "is_email_verified", + "is_managed", + "is_onboarded", + "is_tour_completed", + "mobile_number", + "role", + "onboarding_step", + "user_timezone", + "username", + "theme", + "last_workspace_id", + ] + read_only_fields = fields + + +class UserMeSettingsSerializer(BaseSerializer): + workspace = serializers.SerializerMethodField() + + class Meta: + model = User + fields = [ + "id", + "email", + "workspace", + ] + read_only_fields = fields + + def get_workspace(self, obj): + workspace_invites = WorkspaceMemberInvite.objects.filter( + email=obj.email + ).count() + if obj.last_workspace_id is not None: + workspace = Workspace.objects.filter( + pk=obj.last_workspace_id, workspace_member__member=obj.id + ).first() + return { + "last_workspace_id": obj.last_workspace_id, + "last_workspace_slug": workspace.slug if workspace is not None else "", + "fallback_workspace_id": obj.last_workspace_id, + "fallback_workspace_slug": workspace.slug if workspace is not None else "", + "invites": workspace_invites, + } + else: + fallback_workspace = ( + Workspace.objects.filter(workspace_member__member_id=obj.id) + .order_by("created_at") + .first() + ) + return { + "last_workspace_id": None, + "last_workspace_slug": None, + "fallback_workspace_id": fallback_workspace.id + if fallback_workspace is not None + else None, + "fallback_workspace_slug": fallback_workspace.slug + if fallback_workspace is not None + else None, + "invites": workspace_invites, + } + + class UserLiteSerializer(BaseSerializer): class Meta: model = User @@ -51,7 +126,6 @@ class Meta: class UserAdminLiteSerializer(BaseSerializer): - class Meta: model = User fields = [ diff --git a/apiserver/plane/api/serializers/view.py b/apiserver/plane/api/serializers/view.py index 076228ae098..e7502609a72 100644 --- a/apiserver/plane/api/serializers/view.py +++ b/apiserver/plane/api/serializers/view.py @@ -5,10 +5,39 @@ from .base import BaseSerializer from .workspace import WorkspaceLiteSerializer from .project import ProjectLiteSerializer -from plane.db.models import IssueView, IssueViewFavorite +from plane.db.models import GlobalView, IssueView, IssueViewFavorite from plane.utils.issue_filters import issue_filters +class GlobalViewSerializer(BaseSerializer): + workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + + class Meta: + model = GlobalView + fields = "__all__" + read_only_fields = [ + "workspace", + "query", + ] + + def create(self, validated_data): + query_params = validated_data.get("query_data", {}) + if bool(query_params): + validated_data["query"] = issue_filters(query_params, "POST") + else: + validated_data["query"] = dict() + return GlobalView.objects.create(**validated_data) + + def update(self, instance, validated_data): + query_params = validated_data.get("query_data", {}) + if bool(query_params): + validated_data["query"] = issue_filters(query_params, "POST") + else: + validated_data["query"] = dict() + validated_data["query"] = issue_filters(query_params, "PATCH") + return super().update(instance, validated_data) + + class IssueViewSerializer(BaseSerializer): is_favorite = serializers.BooleanField(read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True) @@ -28,7 +57,7 @@ def create(self, validated_data): if bool(query_params): validated_data["query"] = issue_filters(query_params, "POST") else: - validated_data["query"] = dict() + validated_data["query"] = {} return IssueView.objects.create(**validated_data) def update(self, instance, validated_data): @@ -36,7 +65,7 @@ def update(self, instance, validated_data): if bool(query_params): validated_data["query"] = issue_filters(query_params, "POST") else: - validated_data["query"] = dict() + validated_data["query"] = {} validated_data["query"] = issue_filters(query_params, "PATCH") return super().update(instance, validated_data) diff --git a/apiserver/plane/api/serializers/workspace.py b/apiserver/plane/api/serializers/workspace.py index d27b66481c0..0a80ce8b7be 100644 --- a/apiserver/plane/api/serializers/workspace.py +++ b/apiserver/plane/api/serializers/workspace.py @@ -54,6 +54,13 @@ class Meta: fields = "__all__" +class WorkspaceMemberMeSerializer(BaseSerializer): + + class Meta: + model = WorkspaceMember + fields = "__all__" + + class WorkspaceMemberAdminSerializer(BaseSerializer): member = UserAdminLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) @@ -103,9 +110,8 @@ def create(self, validated_data, **kwargs): ] TeamMember.objects.bulk_create(team_members, batch_size=10) return team - else: - team = Team.objects.create(**validated_data) - return team + team = Team.objects.create(**validated_data) + return team def update(self, instance, validated_data): if "members" in validated_data: @@ -117,8 +123,7 @@ def update(self, instance, validated_data): ] TeamMember.objects.bulk_create(team_members, batch_size=10) return super().update(instance, validated_data) - else: - return super().update(instance, validated_data) + return super().update(instance, validated_data) class WorkspaceThemeSerializer(BaseSerializer): diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py deleted file mode 100644 index 1fb2b8e905c..00000000000 --- a/apiserver/plane/api/urls.py +++ /dev/null @@ -1,1643 +0,0 @@ -from django.urls import path - - -# Create your urls here. - -from plane.api.views import ( - # Authentication - SignUpEndpoint, - SignInEndpoint, - SignOutEndpoint, - MagicSignInEndpoint, - MagicSignInGenerateEndpoint, - OauthEndpoint, - ## End Authentication - # Auth Extended - ForgotPasswordEndpoint, - VerifyEmailEndpoint, - ResetPasswordEndpoint, - RequestEmailVerificationEndpoint, - ChangePasswordEndpoint, - ## End Auth Extender - # User - UserEndpoint, - UpdateUserOnBoardedEndpoint, - UpdateUserTourCompletedEndpoint, - UserActivityEndpoint, - ## End User - # Workspaces - WorkSpaceViewSet, - UserWorkspaceInvitationsEndpoint, - UserWorkSpacesEndpoint, - InviteWorkspaceEndpoint, - JoinWorkspaceEndpoint, - WorkSpaceMemberViewSet, - WorkspaceMembersEndpoint, - WorkspaceInvitationsViewset, - UserWorkspaceInvitationsEndpoint, - WorkspaceMemberUserEndpoint, - WorkspaceMemberUserViewsEndpoint, - WorkSpaceAvailabilityCheckEndpoint, - TeamMemberViewSet, - AddTeamToProjectEndpoint, - UserLastProjectWithWorkspaceEndpoint, - UserWorkspaceInvitationEndpoint, - UserActivityGraphEndpoint, - UserIssueCompletedGraphEndpoint, - UserWorkspaceDashboardEndpoint, - WorkspaceThemeViewSet, - WorkspaceUserProfileStatsEndpoint, - WorkspaceUserActivityEndpoint, - WorkspaceUserProfileEndpoint, - WorkspaceUserProfileIssuesEndpoint, - WorkspaceLabelsEndpoint, - ## End Workspaces - # File Assets - FileAssetEndpoint, - UserAssetsEndpoint, - ## End File Assets - # Projects - ProjectViewSet, - InviteProjectEndpoint, - ProjectMemberViewSet, - ProjectMemberEndpoint, - ProjectMemberInvitationsViewset, - ProjectMemberUserEndpoint, - AddMemberToProjectEndpoint, - ProjectJoinEndpoint, - UserProjectInvitationsViewset, - ProjectIdentifierEndpoint, - ProjectFavoritesViewSet, - ## End Projects - # Issues - IssueViewSet, - WorkSpaceIssuesEndpoint, - IssueActivityEndpoint, - IssueCommentViewSet, - UserWorkSpaceIssues, - BulkDeleteIssuesEndpoint, - BulkImportIssuesEndpoint, - ProjectUserViewsEndpoint, - IssuePropertyViewSet, - LabelViewSet, - SubIssuesEndpoint, - IssueLinkViewSet, - BulkCreateIssueLabelsEndpoint, - IssueAttachmentEndpoint, - IssueArchiveViewSet, - IssueSubscriberViewSet, - IssueCommentPublicViewSet, - IssueReactionViewSet, - CommentReactionViewSet, - ## End Issues - # States - StateViewSet, - ## End States - # Estimates - ProjectEstimatePointEndpoint, - BulkEstimatePointEndpoint, - ## End Estimates - # Views - IssueViewViewSet, - ViewIssuesEndpoint, - IssueViewFavoriteViewSet, - ## End Views - # Cycles - CycleViewSet, - CycleIssueViewSet, - CycleDateCheckEndpoint, - CycleFavoriteViewSet, - TransferCycleIssueEndpoint, - ## End Cycles - # Modules - ModuleViewSet, - ModuleIssueViewSet, - ModuleFavoriteViewSet, - ModuleLinkViewSet, - BulkImportModulesEndpoint, - ## End Modules - # Pages - PageViewSet, - PageBlockViewSet, - PageFavoriteViewSet, - CreateIssueFromPageBlockEndpoint, - ## End Pages - # Api Tokens - ApiTokenEndpoint, - ## End Api Tokens - # Integrations - IntegrationViewSet, - WorkspaceIntegrationViewSet, - GithubRepositoriesEndpoint, - GithubRepositorySyncViewSet, - GithubIssueSyncViewSet, - GithubCommentSyncViewSet, - BulkCreateGithubIssueSyncEndpoint, - SlackProjectSyncViewSet, - ## End Integrations - # Importer - ServiceIssueImportSummaryEndpoint, - ImportServiceEndpoint, - UpdateServiceImportStatusEndpoint, - ## End importer - # Search - GlobalSearchEndpoint, - IssueSearchEndpoint, - ## End Search - # Gpt - GPTIntegrationEndpoint, - ## End Gpt - # Release Notes - ReleaseNotesEndpoint, - ## End Release Notes - # Inbox - InboxViewSet, - InboxIssueViewSet, - ## End Inbox - # Analytics - AnalyticsEndpoint, - AnalyticViewViewset, - SavedAnalyticEndpoint, - ExportAnalyticsEndpoint, - DefaultAnalyticsEndpoint, - ## End Analytics - # Notification - NotificationViewSet, - UnreadNotificationEndpoint, - MarkAllReadNotificationViewSet, - ## End Notification - # Public Boards - ProjectDeployBoardViewSet, - ProjectIssuesPublicEndpoint, - ProjectDeployBoardPublicSettingsEndpoint, - IssueReactionPublicViewSet, - CommentReactionPublicViewSet, - InboxIssuePublicViewSet, - IssueVotePublicViewSet, - WorkspaceProjectDeployBoardEndpoint, - IssueRetrievePublicEndpoint, - ## End Public Boards - ## Exporter - ExportIssuesEndpoint, - ## End Exporter - -) - - -urlpatterns = [ - # Social Auth - path("social-auth/", OauthEndpoint.as_view(), name="oauth"), - # Auth - path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"), - path("sign-in/", SignInEndpoint.as_view(), name="sign-in"), - path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), - # Magic Sign In/Up - path( - "magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate" - ), - path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"), - # Email verification - path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"), - path( - "request-email-verify/", - RequestEmailVerificationEndpoint.as_view(), - name="request-reset-email", - ), - # Password Manipulation - path( - "reset-password///", - ResetPasswordEndpoint.as_view(), - name="password-reset", - ), - path( - "forgot-password/", - ForgotPasswordEndpoint.as_view(), - name="forgot-password", - ), - # User Profile - path( - "users/me/", - UserEndpoint.as_view( - {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} - ), - name="users", - ), - path( - "users/me/change-password/", - ChangePasswordEndpoint.as_view(), - name="change-password", - ), - path( - "users/me/onboard/", - UpdateUserOnBoardedEndpoint.as_view(), - name="user-onboard", - ), - path( - "users/me/tour-completed/", - UpdateUserTourCompletedEndpoint.as_view(), - name="user-tour", - ), - path("users/workspaces//activities/", UserActivityEndpoint.as_view(), name="user-activities"), - # user workspaces - path( - "users/me/workspaces/", - UserWorkSpacesEndpoint.as_view(), - name="user-workspace", - ), - # user workspace invitations - path( - "users/me/invitations/workspaces/", - UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}), - name="user-workspace-invitations", - ), - # user workspace invitation - path( - "users/me/invitations//", - UserWorkspaceInvitationEndpoint.as_view( - { - "get": "retrieve", - } - ), - name="workspace", - ), - # user join workspace - # User Graphs - path( - "users/me/workspaces//activity-graph/", - UserActivityGraphEndpoint.as_view(), - name="user-activity-graph", - ), - path( - "users/me/workspaces//issues-completed-graph/", - UserIssueCompletedGraphEndpoint.as_view(), - name="completed-graph", - ), - path( - "users/me/workspaces//dashboard/", - UserWorkspaceDashboardEndpoint.as_view(), - name="user-workspace-dashboard", - ), - ## User Graph - path( - "users/me/invitations/workspaces///join/", - JoinWorkspaceEndpoint.as_view(), - name="user-join-workspace", - ), - # user project invitations - path( - "users/me/invitations/projects/", - UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}), - name="user-project-invitaions", - ), - ## Workspaces ## - path( - "workspace-slug-check/", - WorkSpaceAvailabilityCheckEndpoint.as_view(), - name="workspace-availability", - ), - path( - "workspaces/", - WorkSpaceViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="workspace", - ), - path( - "workspaces//", - WorkSpaceViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="workspace", - ), - path( - "workspaces//invite/", - InviteWorkspaceEndpoint.as_view(), - name="workspace", - ), - path( - "workspaces//invitations/", - WorkspaceInvitationsViewset.as_view({"get": "list"}), - name="workspace", - ), - path( - "workspaces//invitations//", - WorkspaceInvitationsViewset.as_view( - { - "delete": "destroy", - "get": "retrieve", - } - ), - name="workspace", - ), - path( - "workspaces//members/", - WorkSpaceMemberViewSet.as_view({"get": "list"}), - name="workspace", - ), - path( - "workspaces//members//", - WorkSpaceMemberViewSet.as_view( - { - "patch": "partial_update", - "delete": "destroy", - "get": "retrieve", - } - ), - name="workspace", - ), - path( - "workspaces//workspace-members/", - WorkspaceMembersEndpoint.as_view(), - name="workspace-members", - ), - path( - "workspaces//teams/", - TeamMemberViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="workspace", - ), - path( - "workspaces//teams//", - TeamMemberViewSet.as_view( - { - "put": "update", - "patch": "partial_update", - "delete": "destroy", - "get": "retrieve", - } - ), - name="workspace", - ), - path( - "users/last-visited-workspace/", - UserLastProjectWithWorkspaceEndpoint.as_view(), - name="workspace-project-details", - ), - path( - "workspaces//workspace-members/me/", - WorkspaceMemberUserEndpoint.as_view(), - name="workspace-member-details", - ), - path( - "workspaces//workspace-views/", - WorkspaceMemberUserViewsEndpoint.as_view(), - name="workspace-member-details", - ), - path( - "workspaces//workspace-themes/", - WorkspaceThemeViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="workspace-themes", - ), - path( - "workspaces//workspace-themes//", - WorkspaceThemeViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="workspace-themes", - ), - path( - "workspaces//user-stats//", - WorkspaceUserProfileStatsEndpoint.as_view(), - name="workspace-user-stats", - ), - path( - "workspaces//user-activity//", - WorkspaceUserActivityEndpoint.as_view(), - name="workspace-user-activity", - ), - path( - "workspaces//user-profile//", - WorkspaceUserProfileEndpoint.as_view(), - name="workspace-user-profile-page", - ), - path( - "workspaces//user-issues//", - WorkspaceUserProfileIssuesEndpoint.as_view(), - name="workspace-user-profile-issues", - ), - path( - "workspaces//labels/", - WorkspaceLabelsEndpoint.as_view(), - name="workspace-labels", - ), - ## End Workspaces ## - # Projects - path( - "workspaces//projects/", - ProjectViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project", - ), - path( - "workspaces//projects//", - ProjectViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project", - ), - path( - "workspaces//project-identifiers/", - ProjectIdentifierEndpoint.as_view(), - name="project-identifiers", - ), - path( - "workspaces//projects//invite/", - InviteProjectEndpoint.as_view(), - name="project", - ), - path( - "workspaces//projects//members/", - ProjectMemberViewSet.as_view({"get": "list"}), - name="project", - ), - path( - "workspaces//projects//members//", - ProjectMemberViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project", - ), - path( - "workspaces//projects//project-members/", - ProjectMemberEndpoint.as_view(), - name="project", - ), - path( - "workspaces//projects//members/add/", - AddMemberToProjectEndpoint.as_view(), - name="project", - ), - path( - "workspaces//projects/join/", - ProjectJoinEndpoint.as_view(), - name="project", - ), - path( - "workspaces//projects//team-invite/", - AddTeamToProjectEndpoint.as_view(), - name="projects", - ), - path( - "workspaces//projects//invitations/", - ProjectMemberInvitationsViewset.as_view({"get": "list"}), - name="workspace", - ), - path( - "workspaces//projects//invitations//", - ProjectMemberInvitationsViewset.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - name="project", - ), - path( - "workspaces//projects//project-views/", - ProjectUserViewsEndpoint.as_view(), - name="project-view", - ), - path( - "workspaces//projects//project-members/me/", - ProjectMemberUserEndpoint.as_view(), - name="project-view", - ), - path( - "workspaces//user-favorite-projects/", - ProjectFavoritesViewSet.as_view( - { - "post": "create", - } - ), - name="project", - ), - path( - "workspaces//user-favorite-projects//", - ProjectFavoritesViewSet.as_view( - { - "delete": "destroy", - } - ), - name="project", - ), - # End Projects - # States - path( - "workspaces//projects//states/", - StateViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-states", - ), - path( - "workspaces//projects//states//", - StateViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-state", - ), - # End States ## - # Estimates - path( - "workspaces//projects//project-estimates/", - ProjectEstimatePointEndpoint.as_view(), - name="project-estimate-points", - ), - path( - "workspaces//projects//estimates/", - BulkEstimatePointEndpoint.as_view( - { - "get": "list", - "post": "create", - } - ), - name="bulk-create-estimate-points", - ), - path( - "workspaces//projects//estimates//", - BulkEstimatePointEndpoint.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="bulk-create-estimate-points", - ), - # End Estimates ## - # Views - path( - "workspaces//projects//views/", - IssueViewViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-view", - ), - path( - "workspaces//projects//views//", - IssueViewViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-view", - ), - path( - "workspaces//projects//views//issues/", - ViewIssuesEndpoint.as_view(), - name="project-view-issues", - ), - path( - "workspaces//projects//user-favorite-views/", - IssueViewFavoriteViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="user-favorite-view", - ), - path( - "workspaces//projects//user-favorite-views//", - IssueViewFavoriteViewSet.as_view( - { - "delete": "destroy", - } - ), - name="user-favorite-view", - ), - ## End Views - ## Cycles - path( - "workspaces//projects//cycles/", - CycleViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-cycle", - ), - path( - "workspaces//projects//cycles//", - CycleViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-cycle", - ), - path( - "workspaces//projects//cycles//cycle-issues/", - CycleIssueViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-cycle", - ), - path( - "workspaces//projects//cycles//cycle-issues//", - CycleIssueViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-cycle", - ), - path( - "workspaces//projects//cycles/date-check/", - CycleDateCheckEndpoint.as_view(), - name="project-cycle", - ), - path( - "workspaces//projects//user-favorite-cycles/", - CycleFavoriteViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="user-favorite-cycle", - ), - path( - "workspaces//projects//user-favorite-cycles//", - CycleFavoriteViewSet.as_view( - { - "delete": "destroy", - } - ), - name="user-favorite-cycle", - ), - path( - "workspaces//projects//cycles//transfer-issues/", - TransferCycleIssueEndpoint.as_view(), - name="transfer-issues", - ), - ## End Cycles - # Issue - path( - "workspaces//projects//issues/", - IssueViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue", - ), - path( - "workspaces//projects//issues//", - IssueViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue", - ), - path( - "workspaces//issues/", - WorkSpaceIssuesEndpoint.as_view(), - name="workspace-issue", - ), - path( - "workspaces//projects//issue-labels/", - LabelViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-labels", - ), - path( - "workspaces//projects//issue-labels//", - LabelViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-labels", - ), - path( - "workspaces//projects//bulk-create-labels/", - BulkCreateIssueLabelsEndpoint.as_view(), - name="project-bulk-labels", - ), - path( - "workspaces//projects//bulk-delete-issues/", - BulkDeleteIssuesEndpoint.as_view(), - name="project-issues-bulk", - ), - path( - "workspaces//projects//bulk-import-issues//", - BulkImportIssuesEndpoint.as_view(), - name="project-issues-bulk", - ), - path( - "workspaces//my-issues/", - UserWorkSpaceIssues.as_view(), - name="workspace-issues", - ), - path( - "workspaces//projects//issues//sub-issues/", - SubIssuesEndpoint.as_view(), - name="sub-issues", - ), - path( - "workspaces//projects//issues//issue-links/", - IssueLinkViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-links", - ), - path( - "workspaces//projects//issues//issue-links//", - IssueLinkViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-links", - ), - path( - "workspaces//projects//issues//issue-attachments/", - IssueAttachmentEndpoint.as_view(), - name="project-issue-attachments", - ), - path( - "workspaces//projects//issues//issue-attachments//", - IssueAttachmentEndpoint.as_view(), - name="project-issue-attachments", - ), - path( - "workspaces//export-issues/", - ExportIssuesEndpoint.as_view(), - name="export-issues", - ), - ## End Issues - ## Issue Activity - path( - "workspaces//projects//issues//history/", - IssueActivityEndpoint.as_view(), - name="project-issue-history", - ), - ## Issue Activity - ## IssueComments - path( - "workspaces//projects//issues//comments/", - IssueCommentViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-comment", - ), - path( - "workspaces//projects//issues//comments//", - IssueCommentViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-comment", - ), - ## End IssueComments - # Issue Subscribers - path( - "workspaces//projects//issues//issue-subscribers/", - IssueSubscriberViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-subscribers", - ), - path( - "workspaces//projects//issues//issue-subscribers//", - IssueSubscriberViewSet.as_view({"delete": "destroy"}), - name="project-issue-subscribers", - ), - path( - "workspaces//projects//issues//subscribe/", - IssueSubscriberViewSet.as_view( - { - "get": "subscription_status", - "post": "subscribe", - "delete": "unsubscribe", - } - ), - name="project-issue-subscribers", - ), - ## End Issue Subscribers - # Issue Reactions - path( - "workspaces//projects//issues//reactions/", - IssueReactionViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-reactions", - ), - path( - "workspaces//projects//issues//reactions//", - IssueReactionViewSet.as_view( - { - "delete": "destroy", - } - ), - name="project-issue-reactions", - ), - ## End Issue Reactions - # Comment Reactions - path( - "workspaces//projects//comments//reactions/", - CommentReactionViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-comment-reactions", - ), - path( - "workspaces//projects//comments//reactions//", - CommentReactionViewSet.as_view( - { - "delete": "destroy", - } - ), - name="project-issue-comment-reactions", - ), - ## End Comment Reactions - ## IssueProperty - path( - "workspaces//projects//issue-properties/", - IssuePropertyViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-roadmap", - ), - path( - "workspaces//projects//issue-properties//", - IssuePropertyViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-roadmap", - ), - ## IssueProperty Ebd - ## Issue Archives - path( - "workspaces//projects//archived-issues/", - IssueArchiveViewSet.as_view( - { - "get": "list", - } - ), - name="project-issue-archive", - ), - path( - "workspaces//projects//archived-issues//", - IssueArchiveViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - name="project-issue-archive", - ), - path( - "workspaces//projects//unarchive//", - IssueArchiveViewSet.as_view( - { - "post": "unarchive", - } - ), - name="project-issue-archive", - ), - ## End Issue Archives - ## File Assets - path( - "workspaces//file-assets/", - FileAssetEndpoint.as_view(), - name="file-assets", - ), - path( - "workspaces/file-assets///", - FileAssetEndpoint.as_view(), - name="file-assets", - ), - path( - "users/file-assets/", - UserAssetsEndpoint.as_view(), - name="user-file-assets", - ), - path( - "users/file-assets//", - UserAssetsEndpoint.as_view(), - name="user-file-assets", - ), - ## End File Assets - ## Modules - path( - "workspaces//projects//modules/", - ModuleViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-modules", - ), - path( - "workspaces//projects//modules//", - ModuleViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-modules", - ), - path( - "workspaces//projects//modules//module-issues/", - ModuleIssueViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-module-issues", - ), - path( - "workspaces//projects//modules//module-issues//", - ModuleIssueViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-module-issues", - ), - path( - "workspaces//projects//modules//module-links/", - ModuleLinkViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-issue-module-links", - ), - path( - "workspaces//projects//modules//module-links//", - ModuleLinkViewSet.as_view( - { - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-issue-module-links", - ), - path( - "workspaces//projects//user-favorite-modules/", - ModuleFavoriteViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="user-favorite-module", - ), - path( - "workspaces//projects//user-favorite-modules//", - ModuleFavoriteViewSet.as_view( - { - "delete": "destroy", - } - ), - name="user-favorite-module", - ), - path( - "workspaces//projects//bulk-import-modules//", - BulkImportModulesEndpoint.as_view(), - name="bulk-modules-create", - ), - ## End Modules - # Pages - path( - "workspaces//projects//pages/", - PageViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-pages", - ), - path( - "workspaces//projects//pages//", - PageViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-pages", - ), - path( - "workspaces//projects//pages//page-blocks/", - PageBlockViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-page-blocks", - ), - path( - "workspaces//projects//pages//page-blocks//", - PageBlockViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-page-blocks", - ), - path( - "workspaces//projects//user-favorite-pages/", - PageFavoriteViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="user-favorite-pages", - ), - path( - "workspaces//projects//user-favorite-pages//", - PageFavoriteViewSet.as_view( - { - "delete": "destroy", - } - ), - name="user-favorite-pages", - ), - path( - "workspaces//projects//pages//page-blocks//issues/", - CreateIssueFromPageBlockEndpoint.as_view(), - name="page-block-issues", - ), - ## End Pages - # API Tokens - path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), - path("api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens"), - ## End API Tokens - # Integrations - path( - "integrations/", - IntegrationViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="integrations", - ), - path( - "integrations//", - IntegrationViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="integrations", - ), - path( - "workspaces//workspace-integrations/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "list", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//", - WorkspaceIntegrationViewSet.as_view( - { - "post": "create", - } - ), - name="workspace-integrations", - ), - path( - "workspaces//workspace-integrations//provider/", - WorkspaceIntegrationViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - name="workspace-integrations", - ), - # Github Integrations - path( - "workspaces//workspace-integrations//github-repositories/", - GithubRepositoriesEndpoint.as_view(), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync/", - GithubRepositorySyncViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//github-repository-sync//", - GithubRepositorySyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync/", - GithubIssueSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//bulk-create-github-issue-sync/", - BulkCreateGithubIssueSyncEndpoint.as_view(), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//", - GithubIssueSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync/", - GithubCommentSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync//", - GithubCommentSyncViewSet.as_view( - { - "get": "retrieve", - "delete": "destroy", - } - ), - ), - ## End Github Integrations - # Slack Integration - path( - "workspaces//projects//workspace-integrations//project-slack-sync/", - SlackProjectSyncViewSet.as_view( - { - "post": "create", - "get": "list", - } - ), - ), - path( - "workspaces//projects//workspace-integrations//project-slack-sync//", - SlackProjectSyncViewSet.as_view( - { - "delete": "destroy", - "get": "retrieve", - } - ), - ), - ## End Slack Integration - ## End Integrations - # Importer - path( - "workspaces//importers//", - ServiceIssueImportSummaryEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//projects/importers//", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers/", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//importers///", - ImportServiceEndpoint.as_view(), - name="importer", - ), - path( - "workspaces//projects//service//importers//", - UpdateServiceImportStatusEndpoint.as_view(), - name="importer", - ), - ## End Importer - # Search - path( - "workspaces//search/", - GlobalSearchEndpoint.as_view(), - name="global-search", - ), - path( - "workspaces//projects//search-issues/", - IssueSearchEndpoint.as_view(), - name="project-issue-search", - ), - ## End Search - # Gpt - path( - "workspaces//projects//ai-assistant/", - GPTIntegrationEndpoint.as_view(), - name="importer", - ), - ## End Gpt - # Release Notes - path( - "release-notes/", - ReleaseNotesEndpoint.as_view(), - name="release-notes", - ), - ## End Release Notes - # Inbox - path( - "workspaces//projects//inboxes/", - InboxViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="inbox", - ), - path( - "workspaces//projects//inboxes//", - InboxViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="inbox", - ), - path( - "workspaces//projects//inboxes//inbox-issues/", - InboxIssueViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="inbox-issue", - ), - path( - "workspaces//projects//inboxes//inbox-issues//", - InboxIssueViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="inbox-issue", - ), - ## End Inbox - # Analytics - path( - "workspaces//analytics/", - AnalyticsEndpoint.as_view(), - name="plane-analytics", - ), - path( - "workspaces//analytic-view/", - AnalyticViewViewset.as_view({"get": "list", "post": "create"}), - name="analytic-view", - ), - path( - "workspaces//analytic-view//", - AnalyticViewViewset.as_view( - {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} - ), - name="analytic-view", - ), - path( - "workspaces//saved-analytic-view//", - SavedAnalyticEndpoint.as_view(), - name="saved-analytic-view", - ), - path( - "workspaces//export-analytics/", - ExportAnalyticsEndpoint.as_view(), - name="export-analytics", - ), - path( - "workspaces//default-analytics/", - DefaultAnalyticsEndpoint.as_view(), - name="default-analytics", - ), - ## End Analytics - # Notification - path( - "workspaces//users/notifications/", - NotificationViewSet.as_view( - { - "get": "list", - } - ), - name="notifications", - ), - path( - "workspaces//users/notifications//", - NotificationViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="notifications", - ), - path( - "workspaces//users/notifications//read/", - NotificationViewSet.as_view( - { - "post": "mark_read", - "delete": "mark_unread", - } - ), - name="notifications", - ), - path( - "workspaces//users/notifications//archive/", - NotificationViewSet.as_view( - { - "post": "archive", - "delete": "unarchive", - } - ), - name="notifications", - ), - path( - "workspaces//users/notifications/unread/", - UnreadNotificationEndpoint.as_view(), - name="unread-notifications", - ), - path( - "workspaces//users/notifications/mark-all-read/", - MarkAllReadNotificationViewSet.as_view( - { - "post": "create", - } - ), - name="mark-all-read-notifications", - ), - ## End Notification - # Public Boards - path( - "workspaces//projects//project-deploy-boards/", - ProjectDeployBoardViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-deploy-board", - ), - path( - "workspaces//projects//project-deploy-boards//", - ProjectDeployBoardViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-deploy-board", - ), - path( - "public/workspaces//project-boards//settings/", - ProjectDeployBoardPublicSettingsEndpoint.as_view(), - name="project-deploy-board-settings", - ), - path( - "public/workspaces//project-boards//issues/", - ProjectIssuesPublicEndpoint.as_view(), - name="project-deploy-board", - ), - path( - "public/workspaces//project-boards//issues//", - IssueRetrievePublicEndpoint.as_view(), - name="workspace-project-boards", - ), - path( - "public/workspaces//project-boards//issues//comments/", - IssueCommentPublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="issue-comments-project-board", - ), - path( - "public/workspaces//project-boards//issues//comments//", - IssueCommentPublicViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="issue-comments-project-board", - ), - path( - "public/workspaces//project-boards//issues//reactions/", - IssueReactionPublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="issue-reactions-project-board", - ), - path( - "public/workspaces//project-boards//issues//reactions//", - IssueReactionPublicViewSet.as_view( - { - "delete": "destroy", - } - ), - name="issue-reactions-project-board", - ), - path( - "public/workspaces//project-boards//comments//reactions/", - CommentReactionPublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="comment-reactions-project-board", - ), - path( - "public/workspaces//project-boards//comments//reactions//", - CommentReactionPublicViewSet.as_view( - { - "delete": "destroy", - } - ), - name="comment-reactions-project-board", - ), - path( - "public/workspaces//project-boards//inboxes//inbox-issues/", - InboxIssuePublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="inbox-issue", - ), - path( - "public/workspaces//project-boards//inboxes//inbox-issues//", - InboxIssuePublicViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="inbox-issue", - ), - path( - "public/workspaces//project-boards//issues//votes/", - IssueVotePublicViewSet.as_view( - { - "get": "list", - "post": "create", - "delete": "destroy", - } - ), - name="issue-vote-project-board", - ), - path( - "public/workspaces//project-boards/", - WorkspaceProjectDeployBoardEndpoint.as_view(), - name="workspace-project-boards", - ), - ## End Public Boards -] diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py new file mode 100644 index 00000000000..e4f3718f59c --- /dev/null +++ b/apiserver/plane/api/urls/__init__.py @@ -0,0 +1,50 @@ +from .analytic import urlpatterns as analytic_urls +from .asset import urlpatterns as asset_urls +from .authentication import urlpatterns as authentication_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 +from .importer import urlpatterns as importer_urls +from .inbox import urlpatterns as inbox_urls +from .integration import urlpatterns as integration_urls +from .issue import urlpatterns as issue_urls +from .module import urlpatterns as module_urls +from .notification import urlpatterns as notification_urls +from .page import urlpatterns as page_urls +from .project import urlpatterns as project_urls +from .public_board import urlpatterns as public_board_urls +from .release_note import urlpatterns as release_note_urls +from .search import urlpatterns as search_urls +from .state import urlpatterns as state_urls +from .unsplash import urlpatterns as unsplash_urls +from .user import urlpatterns as user_urls +from .views import urlpatterns as view_urls +from .workspace import urlpatterns as workspace_urls + + +urlpatterns = [ + *analytic_urls, + *asset_urls, + *authentication_urls, + *configuration_urls, + *cycle_urls, + *estimate_urls, + *gpt_urls, + *importer_urls, + *inbox_urls, + *integration_urls, + *issue_urls, + *module_urls, + *notification_urls, + *page_urls, + *project_urls, + *public_board_urls, + *release_note_urls, + *search_urls, + *state_urls, + *unsplash_urls, + *user_urls, + *view_urls, + *workspace_urls, +] diff --git a/apiserver/plane/api/urls/analytic.py b/apiserver/plane/api/urls/analytic.py new file mode 100644 index 00000000000..cb6155e3256 --- /dev/null +++ b/apiserver/plane/api/urls/analytic.py @@ -0,0 +1,46 @@ +from django.urls import path + + +from plane.api.views import ( + AnalyticsEndpoint, + AnalyticViewViewset, + SavedAnalyticEndpoint, + ExportAnalyticsEndpoint, + DefaultAnalyticsEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//analytics/", + AnalyticsEndpoint.as_view(), + name="plane-analytics", + ), + path( + "workspaces//analytic-view/", + AnalyticViewViewset.as_view({"get": "list", "post": "create"}), + name="analytic-view", + ), + path( + "workspaces//analytic-view//", + AnalyticViewViewset.as_view( + {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} + ), + name="analytic-view", + ), + path( + "workspaces//saved-analytic-view//", + SavedAnalyticEndpoint.as_view(), + name="saved-analytic-view", + ), + path( + "workspaces//export-analytics/", + ExportAnalyticsEndpoint.as_view(), + name="export-analytics", + ), + path( + "workspaces//default-analytics/", + DefaultAnalyticsEndpoint.as_view(), + name="default-analytics", + ), +] diff --git a/apiserver/plane/api/urls/asset.py b/apiserver/plane/api/urls/asset.py new file mode 100644 index 00000000000..b6ae9f42c4a --- /dev/null +++ b/apiserver/plane/api/urls/asset.py @@ -0,0 +1,31 @@ +from django.urls import path + + +from plane.api.views import ( + FileAssetEndpoint, + UserAssetsEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//file-assets/", + FileAssetEndpoint.as_view(), + name="file-assets", + ), + path( + "workspaces/file-assets///", + FileAssetEndpoint.as_view(), + name="file-assets", + ), + path( + "users/file-assets/", + UserAssetsEndpoint.as_view(), + name="user-file-assets", + ), + path( + "users/file-assets//", + UserAssetsEndpoint.as_view(), + name="user-file-assets", + ), +] diff --git a/apiserver/plane/api/urls/authentication.py b/apiserver/plane/api/urls/authentication.py new file mode 100644 index 00000000000..44b7000eac9 --- /dev/null +++ b/apiserver/plane/api/urls/authentication.py @@ -0,0 +1,68 @@ +from django.urls import path + +from rest_framework_simplejwt.views import TokenRefreshView + + +from plane.api.views import ( + # Authentication + SignUpEndpoint, + SignInEndpoint, + SignOutEndpoint, + MagicSignInEndpoint, + MagicSignInGenerateEndpoint, + OauthEndpoint, + ## End Authentication + # Auth Extended + ForgotPasswordEndpoint, + VerifyEmailEndpoint, + ResetPasswordEndpoint, + RequestEmailVerificationEndpoint, + ChangePasswordEndpoint, + ## End Auth Extender + # API Tokens + ApiTokenEndpoint, + ## End API Tokens +) + + +urlpatterns = [ + # Social Auth + path("social-auth/", OauthEndpoint.as_view(), name="oauth"), + # Auth + path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"), + path("sign-in/", SignInEndpoint.as_view(), name="sign-in"), + path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), + # Magic Sign In/Up + path( + "magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate" + ), + path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + # Email verification + path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"), + path( + "request-email-verify/", + RequestEmailVerificationEndpoint.as_view(), + name="request-reset-email", + ), + # Password Manipulation + path( + "users/me/change-password/", + ChangePasswordEndpoint.as_view(), + name="change-password", + ), + path( + "reset-password///", + ResetPasswordEndpoint.as_view(), + name="password-reset", + ), + path( + "forgot-password/", + ForgotPasswordEndpoint.as_view(), + name="forgot-password", + ), + # API Tokens + path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), + path("api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens"), + ## End API Tokens +] diff --git a/apiserver/plane/api/urls/config.py b/apiserver/plane/api/urls/config.py new file mode 100644 index 00000000000..321a5620062 --- /dev/null +++ b/apiserver/plane/api/urls/config.py @@ -0,0 +1,12 @@ +from django.urls import path + + +from plane.api.views import ConfigurationEndpoint + +urlpatterns = [ + path( + "configs/", + ConfigurationEndpoint.as_view(), + name="configuration", + ), +] \ No newline at end of file diff --git a/apiserver/plane/api/urls/cycle.py b/apiserver/plane/api/urls/cycle.py new file mode 100644 index 00000000000..068276361a2 --- /dev/null +++ b/apiserver/plane/api/urls/cycle.py @@ -0,0 +1,87 @@ +from django.urls import path + + +from plane.api.views import ( + CycleViewSet, + CycleIssueViewSet, + CycleDateCheckEndpoint, + CycleFavoriteViewSet, + TransferCycleIssueEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//projects//cycles/", + CycleViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-cycle", + ), + path( + "workspaces//projects//cycles//", + CycleViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-cycle", + ), + path( + "workspaces//projects//cycles//cycle-issues/", + CycleIssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-cycle", + ), + path( + "workspaces//projects//cycles//cycle-issues//", + CycleIssueViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-cycle", + ), + path( + "workspaces//projects//cycles/date-check/", + CycleDateCheckEndpoint.as_view(), + name="project-cycle-date", + ), + path( + "workspaces//projects//user-favorite-cycles/", + CycleFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-cycle", + ), + path( + "workspaces//projects//user-favorite-cycles//", + CycleFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-cycle", + ), + path( + "workspaces//projects//cycles//transfer-issues/", + TransferCycleIssueEndpoint.as_view(), + name="transfer-issues", + ), +] diff --git a/apiserver/plane/api/urls/estimate.py b/apiserver/plane/api/urls/estimate.py new file mode 100644 index 00000000000..89363e849b3 --- /dev/null +++ b/apiserver/plane/api/urls/estimate.py @@ -0,0 +1,37 @@ +from django.urls import path + + +from plane.api.views import ( + ProjectEstimatePointEndpoint, + BulkEstimatePointEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//projects//project-estimates/", + ProjectEstimatePointEndpoint.as_view(), + name="project-estimate-points", + ), + path( + "workspaces//projects//estimates/", + BulkEstimatePointEndpoint.as_view( + { + "get": "list", + "post": "create", + } + ), + name="bulk-create-estimate-points", + ), + path( + "workspaces//projects//estimates//", + BulkEstimatePointEndpoint.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="bulk-create-estimate-points", + ), +] diff --git a/apiserver/plane/api/urls/gpt.py b/apiserver/plane/api/urls/gpt.py new file mode 100644 index 00000000000..f2b0362c766 --- /dev/null +++ b/apiserver/plane/api/urls/gpt.py @@ -0,0 +1,13 @@ +from django.urls import path + + +from plane.api.views import GPTIntegrationEndpoint + + +urlpatterns = [ + path( + "workspaces//projects//ai-assistant/", + GPTIntegrationEndpoint.as_view(), + name="importer", + ), +] diff --git a/apiserver/plane/api/urls/importer.py b/apiserver/plane/api/urls/importer.py new file mode 100644 index 00000000000..c0a9aa5b529 --- /dev/null +++ b/apiserver/plane/api/urls/importer.py @@ -0,0 +1,37 @@ +from django.urls import path + + +from plane.api.views import ( + ServiceIssueImportSummaryEndpoint, + ImportServiceEndpoint, + UpdateServiceImportStatusEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//importers//", + ServiceIssueImportSummaryEndpoint.as_view(), + name="importer-summary", + ), + path( + "workspaces//projects/importers//", + ImportServiceEndpoint.as_view(), + name="importer", + ), + path( + "workspaces//importers/", + ImportServiceEndpoint.as_view(), + name="importer", + ), + path( + "workspaces//importers///", + ImportServiceEndpoint.as_view(), + name="importer", + ), + path( + "workspaces//projects//service//importers//", + UpdateServiceImportStatusEndpoint.as_view(), + name="importer-status", + ), +] diff --git a/apiserver/plane/api/urls/inbox.py b/apiserver/plane/api/urls/inbox.py new file mode 100644 index 00000000000..315f30601b6 --- /dev/null +++ b/apiserver/plane/api/urls/inbox.py @@ -0,0 +1,53 @@ +from django.urls import path + + +from plane.api.views import ( + InboxViewSet, + InboxIssueViewSet, +) + + +urlpatterns = [ + path( + "workspaces//projects//inboxes/", + InboxViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox", + ), + path( + "workspaces//projects//inboxes//", + InboxViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox", + ), + path( + "workspaces//projects//inboxes//inbox-issues/", + InboxIssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox-issue", + ), + path( + "workspaces//projects//inboxes//inbox-issues//", + InboxIssueViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox-issue", + ), +] diff --git a/apiserver/plane/api/urls/integration.py b/apiserver/plane/api/urls/integration.py new file mode 100644 index 00000000000..dd431b6c866 --- /dev/null +++ b/apiserver/plane/api/urls/integration.py @@ -0,0 +1,150 @@ +from django.urls import path + + +from plane.api.views import ( + IntegrationViewSet, + WorkspaceIntegrationViewSet, + GithubRepositoriesEndpoint, + GithubRepositorySyncViewSet, + GithubIssueSyncViewSet, + GithubCommentSyncViewSet, + BulkCreateGithubIssueSyncEndpoint, + SlackProjectSyncViewSet, +) + + +urlpatterns = [ + path( + "integrations/", + IntegrationViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="integrations", + ), + path( + "integrations//", + IntegrationViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="integrations", + ), + path( + "workspaces//workspace-integrations/", + WorkspaceIntegrationViewSet.as_view( + { + "get": "list", + } + ), + name="workspace-integrations", + ), + path( + "workspaces//workspace-integrations//", + WorkspaceIntegrationViewSet.as_view( + { + "post": "create", + } + ), + name="workspace-integrations", + ), + path( + "workspaces//workspace-integrations//provider/", + WorkspaceIntegrationViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + name="workspace-integrations", + ), + # Github Integrations + path( + "workspaces//workspace-integrations//github-repositories/", + GithubRepositoriesEndpoint.as_view(), + ), + path( + "workspaces//projects//workspace-integrations//github-repository-sync/", + GithubRepositorySyncViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + ), + path( + "workspaces//projects//workspace-integrations//github-repository-sync//", + GithubRepositorySyncViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync/", + GithubIssueSyncViewSet.as_view( + { + "post": "create", + "get": "list", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//bulk-create-github-issue-sync/", + BulkCreateGithubIssueSyncEndpoint.as_view(), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync//", + GithubIssueSyncViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync/", + GithubCommentSyncViewSet.as_view( + { + "post": "create", + "get": "list", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync//", + GithubCommentSyncViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + ), + ## End Github Integrations + # Slack Integration + path( + "workspaces//projects//workspace-integrations//project-slack-sync/", + SlackProjectSyncViewSet.as_view( + { + "post": "create", + "get": "list", + } + ), + ), + path( + "workspaces//projects//workspace-integrations//project-slack-sync//", + SlackProjectSyncViewSet.as_view( + { + "delete": "destroy", + "get": "retrieve", + } + ), + ), + ## End Slack Integration +] diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py new file mode 100644 index 00000000000..f1ef7c1767e --- /dev/null +++ b/apiserver/plane/api/urls/issue.py @@ -0,0 +1,315 @@ +from django.urls import path + + +from plane.api.views import ( + IssueViewSet, + LabelViewSet, + BulkCreateIssueLabelsEndpoint, + BulkDeleteIssuesEndpoint, + BulkImportIssuesEndpoint, + UserWorkSpaceIssues, + SubIssuesEndpoint, + IssueLinkViewSet, + IssueAttachmentEndpoint, + ExportIssuesEndpoint, + IssueActivityEndpoint, + IssueCommentViewSet, + IssueSubscriberViewSet, + IssueReactionViewSet, + CommentReactionViewSet, + IssueUserDisplayPropertyEndpoint, + IssueArchiveViewSet, + IssueRelationViewSet, + IssueDraftViewSet, +) + + +urlpatterns = [ + path( + "workspaces//projects//issues/", + IssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue", + ), + path( + "workspaces//projects//issues//", + IssueViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue", + ), + path( + "workspaces//projects//issue-labels/", + LabelViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-labels", + ), + path( + "workspaces//projects//issue-labels//", + LabelViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-labels", + ), + path( + "workspaces//projects//bulk-create-labels/", + BulkCreateIssueLabelsEndpoint.as_view(), + name="project-bulk-labels", + ), + path( + "workspaces//projects//bulk-delete-issues/", + BulkDeleteIssuesEndpoint.as_view(), + name="project-issues-bulk", + ), + path( + "workspaces//projects//bulk-import-issues//", + BulkImportIssuesEndpoint.as_view(), + name="project-issues-bulk", + ), + path( + "workspaces//my-issues/", + UserWorkSpaceIssues.as_view(), + name="workspace-issues", + ), + path( + "workspaces//projects//issues//sub-issues/", + SubIssuesEndpoint.as_view(), + name="sub-issues", + ), + path( + "workspaces//projects//issues//issue-links/", + IssueLinkViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-links", + ), + path( + "workspaces//projects//issues//issue-links//", + IssueLinkViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-links", + ), + path( + "workspaces//projects//issues//issue-attachments/", + IssueAttachmentEndpoint.as_view(), + name="project-issue-attachments", + ), + path( + "workspaces//projects//issues//issue-attachments//", + IssueAttachmentEndpoint.as_view(), + name="project-issue-attachments", + ), + path( + "workspaces//export-issues/", + ExportIssuesEndpoint.as_view(), + name="export-issues", + ), + ## End Issues + ## Issue Activity + path( + "workspaces//projects//issues//history/", + IssueActivityEndpoint.as_view(), + name="project-issue-history", + ), + ## Issue Activity + ## IssueComments + path( + "workspaces//projects//issues//comments/", + IssueCommentViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-comment", + ), + path( + "workspaces//projects//issues//comments//", + IssueCommentViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-comment", + ), + ## End IssueComments + # Issue Subscribers + path( + "workspaces//projects//issues//issue-subscribers/", + IssueSubscriberViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-subscribers", + ), + path( + "workspaces//projects//issues//issue-subscribers//", + IssueSubscriberViewSet.as_view({"delete": "destroy"}), + name="project-issue-subscribers", + ), + path( + "workspaces//projects//issues//subscribe/", + IssueSubscriberViewSet.as_view( + { + "get": "subscription_status", + "post": "subscribe", + "delete": "unsubscribe", + } + ), + name="project-issue-subscribers", + ), + ## End Issue Subscribers + # Issue Reactions + path( + "workspaces//projects//issues//reactions/", + IssueReactionViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-reactions", + ), + path( + "workspaces//projects//issues//reactions//", + IssueReactionViewSet.as_view( + { + "delete": "destroy", + } + ), + name="project-issue-reactions", + ), + ## End Issue Reactions + # Comment Reactions + path( + "workspaces//projects//comments//reactions/", + CommentReactionViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-comment-reactions", + ), + path( + "workspaces//projects//comments//reactions//", + CommentReactionViewSet.as_view( + { + "delete": "destroy", + } + ), + name="project-issue-comment-reactions", + ), + ## End Comment Reactions + ## IssueProperty + path( + "workspaces//projects//issue-display-properties/", + IssueUserDisplayPropertyEndpoint.as_view(), + name="project-issue-display-properties", + ), + ## IssueProperty End + ## Issue Archives + path( + "workspaces//projects//archived-issues/", + IssueArchiveViewSet.as_view( + { + "get": "list", + } + ), + name="project-issue-archive", + ), + path( + "workspaces//projects//archived-issues//", + IssueArchiveViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + name="project-issue-archive", + ), + path( + "workspaces//projects//unarchive//", + IssueArchiveViewSet.as_view( + { + "post": "unarchive", + } + ), + name="project-issue-archive", + ), + ## End Issue Archives + ## Issue Relation + path( + "workspaces//projects//issues//issue-relation/", + IssueRelationViewSet.as_view( + { + "post": "create", + } + ), + name="issue-relation", + ), + path( + "workspaces//projects//issues//issue-relation//", + IssueRelationViewSet.as_view( + { + "delete": "destroy", + } + ), + name="issue-relation", + ), + ## End Issue Relation + ## Issue Drafts + path( + "workspaces//projects//issue-drafts/", + IssueDraftViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-draft", + ), + path( + "workspaces//projects//issue-drafts//", + IssueDraftViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-draft", + ), +] diff --git a/apiserver/plane/api/urls/module.py b/apiserver/plane/api/urls/module.py new file mode 100644 index 00000000000..3239af1e45b --- /dev/null +++ b/apiserver/plane/api/urls/module.py @@ -0,0 +1,104 @@ +from django.urls import path + + +from plane.api.views import ( + ModuleViewSet, + ModuleIssueViewSet, + ModuleLinkViewSet, + ModuleFavoriteViewSet, + BulkImportModulesEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//projects//modules/", + ModuleViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-modules", + ), + path( + "workspaces//projects//modules//", + ModuleViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-modules", + ), + path( + "workspaces//projects//modules//module-issues/", + ModuleIssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-module-issues", + ), + path( + "workspaces//projects//modules//module-issues//", + ModuleIssueViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-module-issues", + ), + path( + "workspaces//projects//modules//module-links/", + ModuleLinkViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-module-links", + ), + path( + "workspaces//projects//modules//module-links//", + ModuleLinkViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-module-links", + ), + path( + "workspaces//projects//user-favorite-modules/", + ModuleFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-module", + ), + path( + "workspaces//projects//user-favorite-modules//", + ModuleFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-module", + ), + path( + "workspaces//projects//bulk-import-modules//", + BulkImportModulesEndpoint.as_view(), + name="bulk-modules-create", + ), +] diff --git a/apiserver/plane/api/urls/notification.py b/apiserver/plane/api/urls/notification.py new file mode 100644 index 00000000000..5e1936d0160 --- /dev/null +++ b/apiserver/plane/api/urls/notification.py @@ -0,0 +1,66 @@ +from django.urls import path + + +from plane.api.views import ( + NotificationViewSet, + UnreadNotificationEndpoint, + MarkAllReadNotificationViewSet, +) + + +urlpatterns = [ + path( + "workspaces//users/notifications/", + NotificationViewSet.as_view( + { + "get": "list", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications//", + NotificationViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications//read/", + NotificationViewSet.as_view( + { + "post": "mark_read", + "delete": "mark_unread", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications//archive/", + NotificationViewSet.as_view( + { + "post": "archive", + "delete": "unarchive", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications/unread/", + UnreadNotificationEndpoint.as_view(), + name="unread-notifications", + ), + path( + "workspaces//users/notifications/mark-all-read/", + MarkAllReadNotificationViewSet.as_view( + { + "post": "create", + } + ), + name="mark-all-read-notifications", + ), +] diff --git a/apiserver/plane/api/urls/page.py b/apiserver/plane/api/urls/page.py new file mode 100644 index 00000000000..64870228353 --- /dev/null +++ b/apiserver/plane/api/urls/page.py @@ -0,0 +1,79 @@ +from django.urls import path + + +from plane.api.views import ( + PageViewSet, + PageBlockViewSet, + PageFavoriteViewSet, + CreateIssueFromPageBlockEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//projects//pages/", + PageViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-pages", + ), + path( + "workspaces//projects//pages//", + PageViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-pages", + ), + path( + "workspaces//projects//pages//page-blocks/", + PageBlockViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-page-blocks", + ), + path( + "workspaces//projects//pages//page-blocks//", + PageBlockViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-page-blocks", + ), + path( + "workspaces//projects//user-favorite-pages/", + PageFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-pages", + ), + path( + "workspaces//projects//user-favorite-pages//", + PageFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-pages", + ), + path( + "workspaces//projects//pages//page-blocks//issues/", + CreateIssueFromPageBlockEndpoint.as_view(), + name="page-block-issues", + ), +] diff --git a/apiserver/plane/api/urls/project.py b/apiserver/plane/api/urls/project.py new file mode 100644 index 00000000000..2d9e513df28 --- /dev/null +++ b/apiserver/plane/api/urls/project.py @@ -0,0 +1,132 @@ +from django.urls import path + +from plane.api.views import ( + ProjectViewSet, + InviteProjectEndpoint, + ProjectMemberViewSet, + ProjectMemberInvitationsViewset, + ProjectMemberUserEndpoint, + ProjectJoinEndpoint, + AddTeamToProjectEndpoint, + ProjectUserViewsEndpoint, + ProjectIdentifierEndpoint, + ProjectFavoritesViewSet, + LeaveProjectEndpoint, + ProjectPublicCoverImagesEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//projects/", + ProjectViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project", + ), + path( + "workspaces//projects//", + ProjectViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project", + ), + path( + "workspaces//project-identifiers/", + ProjectIdentifierEndpoint.as_view(), + name="project-identifiers", + ), + path( + "workspaces//projects//invite/", + InviteProjectEndpoint.as_view(), + name="invite-project", + ), + path( + "workspaces//projects//members/", + ProjectMemberViewSet.as_view({"get": "list", "post": "create"}), + name="project-member", + ), + path( + "workspaces//projects//members//", + ProjectMemberViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-member", + ), + path( + "workspaces//projects/join/", + ProjectJoinEndpoint.as_view(), + name="project-join", + ), + path( + "workspaces//projects//team-invite/", + AddTeamToProjectEndpoint.as_view(), + name="projects", + ), + path( + "workspaces//projects//invitations/", + ProjectMemberInvitationsViewset.as_view({"get": "list"}), + name="project-member-invite", + ), + path( + "workspaces//projects//invitations//", + ProjectMemberInvitationsViewset.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + name="project-member-invite", + ), + path( + "workspaces//projects//project-views/", + ProjectUserViewsEndpoint.as_view(), + name="project-view", + ), + path( + "workspaces//projects//project-members/me/", + ProjectMemberUserEndpoint.as_view(), + name="project-member-view", + ), + path( + "workspaces//user-favorite-projects/", + ProjectFavoritesViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-favorite", + ), + path( + "workspaces//user-favorite-projects//", + ProjectFavoritesViewSet.as_view( + { + "delete": "destroy", + } + ), + name="project-favorite", + ), + path( + "workspaces//projects//members/leave/", + LeaveProjectEndpoint.as_view(), + name="leave-project", + ), + path( + "project-covers/", + ProjectPublicCoverImagesEndpoint.as_view(), + name="project-covers", + ), +] diff --git a/apiserver/plane/api/urls/public_board.py b/apiserver/plane/api/urls/public_board.py new file mode 100644 index 00000000000..272d5961c56 --- /dev/null +++ b/apiserver/plane/api/urls/public_board.py @@ -0,0 +1,151 @@ +from django.urls import path + + +from plane.api.views import ( + ProjectDeployBoardViewSet, + ProjectDeployBoardPublicSettingsEndpoint, + ProjectIssuesPublicEndpoint, + IssueRetrievePublicEndpoint, + IssueCommentPublicViewSet, + IssueReactionPublicViewSet, + CommentReactionPublicViewSet, + InboxIssuePublicViewSet, + IssueVotePublicViewSet, + WorkspaceProjectDeployBoardEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//projects//project-deploy-boards/", + ProjectDeployBoardViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-deploy-board", + ), + path( + "workspaces//projects//project-deploy-boards//", + ProjectDeployBoardViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-deploy-board", + ), + path( + "public/workspaces//project-boards//settings/", + ProjectDeployBoardPublicSettingsEndpoint.as_view(), + name="project-deploy-board-settings", + ), + path( + "public/workspaces//project-boards//issues/", + ProjectIssuesPublicEndpoint.as_view(), + name="project-deploy-board", + ), + path( + "public/workspaces//project-boards//issues//", + IssueRetrievePublicEndpoint.as_view(), + name="workspace-project-boards", + ), + path( + "public/workspaces//project-boards//issues//comments/", + IssueCommentPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="issue-comments-project-board", + ), + path( + "public/workspaces//project-boards//issues//comments//", + IssueCommentPublicViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="issue-comments-project-board", + ), + path( + "public/workspaces//project-boards//issues//reactions/", + IssueReactionPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="issue-reactions-project-board", + ), + path( + "public/workspaces//project-boards//issues//reactions//", + IssueReactionPublicViewSet.as_view( + { + "delete": "destroy", + } + ), + name="issue-reactions-project-board", + ), + path( + "public/workspaces//project-boards//comments//reactions/", + CommentReactionPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="comment-reactions-project-board", + ), + path( + "public/workspaces//project-boards//comments//reactions//", + CommentReactionPublicViewSet.as_view( + { + "delete": "destroy", + } + ), + name="comment-reactions-project-board", + ), + path( + "public/workspaces//project-boards//inboxes//inbox-issues/", + InboxIssuePublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox-issue", + ), + path( + "public/workspaces//project-boards//inboxes//inbox-issues//", + InboxIssuePublicViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox-issue", + ), + path( + "public/workspaces//project-boards//issues//votes/", + IssueVotePublicViewSet.as_view( + { + "get": "list", + "post": "create", + "delete": "destroy", + } + ), + name="issue-vote-project-board", + ), + path( + "public/workspaces//project-boards/", + WorkspaceProjectDeployBoardEndpoint.as_view(), + name="workspace-project-boards", + ), +] diff --git a/apiserver/plane/api/urls/release_note.py b/apiserver/plane/api/urls/release_note.py new file mode 100644 index 00000000000..dfbd1ec66df --- /dev/null +++ b/apiserver/plane/api/urls/release_note.py @@ -0,0 +1,13 @@ +from django.urls import path + + +from plane.api.views import ReleaseNotesEndpoint + + +urlpatterns = [ + path( + "release-notes/", + ReleaseNotesEndpoint.as_view(), + name="release-notes", + ), +] diff --git a/apiserver/plane/api/urls/search.py b/apiserver/plane/api/urls/search.py new file mode 100644 index 00000000000..282feb04667 --- /dev/null +++ b/apiserver/plane/api/urls/search.py @@ -0,0 +1,21 @@ +from django.urls import path + + +from plane.api.views import ( + GlobalSearchEndpoint, + IssueSearchEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//search/", + GlobalSearchEndpoint.as_view(), + name="global-search", + ), + path( + "workspaces//projects//search-issues/", + IssueSearchEndpoint.as_view(), + name="project-issue-search", + ), +] diff --git a/apiserver/plane/api/urls/state.py b/apiserver/plane/api/urls/state.py new file mode 100644 index 00000000000..bcfd80cd7cd --- /dev/null +++ b/apiserver/plane/api/urls/state.py @@ -0,0 +1,30 @@ +from django.urls import path + + +from plane.api.views import StateViewSet + + +urlpatterns = [ + path( + "workspaces//projects//states/", + StateViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-states", + ), + path( + "workspaces//projects//states//", + StateViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-state", + ), +] diff --git a/apiserver/plane/api/urls/unsplash.py b/apiserver/plane/api/urls/unsplash.py new file mode 100644 index 00000000000..25fab4694d0 --- /dev/null +++ b/apiserver/plane/api/urls/unsplash.py @@ -0,0 +1,13 @@ +from django.urls import path + + +from plane.api.views import UnsplashEndpoint + + +urlpatterns = [ + path( + "unsplash/", + UnsplashEndpoint.as_view(), + name="unsplash", + ), +] diff --git a/apiserver/plane/api/urls/user.py b/apiserver/plane/api/urls/user.py new file mode 100644 index 00000000000..5282a7cf6d3 --- /dev/null +++ b/apiserver/plane/api/urls/user.py @@ -0,0 +1,113 @@ +from django.urls import path + +from plane.api.views import ( + ## User + UserEndpoint, + UpdateUserOnBoardedEndpoint, + UpdateUserTourCompletedEndpoint, + UserActivityEndpoint, + ChangePasswordEndpoint, + ## End User + ## Workspaces + UserWorkspaceInvitationsEndpoint, + UserWorkSpacesEndpoint, + JoinWorkspaceEndpoint, + UserWorkspaceInvitationsEndpoint, + UserWorkspaceInvitationEndpoint, + UserActivityGraphEndpoint, + UserIssueCompletedGraphEndpoint, + UserWorkspaceDashboardEndpoint, + UserProjectInvitationsViewset, + ## End Workspaces +) + +urlpatterns = [ + # User Profile + path( + "users/me/", + UserEndpoint.as_view( + {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} + ), + name="users", + ), + path( + "users/me/settings/", + UserEndpoint.as_view( + { + "get": "retrieve_user_settings", + } + ), + name="users", + ), + path( + "users/me/change-password/", + ChangePasswordEndpoint.as_view(), + name="change-password", + ), + path( + "users/me/onboard/", + UpdateUserOnBoardedEndpoint.as_view(), + name="user-onboard", + ), + path( + "users/me/tour-completed/", + UpdateUserTourCompletedEndpoint.as_view(), + name="user-tour", + ), + path( + "users/workspaces//activities/", + UserActivityEndpoint.as_view(), + name="user-activities", + ), + # user workspaces + path( + "users/me/workspaces/", + UserWorkSpacesEndpoint.as_view(), + name="user-workspace", + ), + # user workspace invitations + path( + "users/me/invitations/workspaces/", + UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}), + name="user-workspace-invitations", + ), + # user workspace invitation + path( + "users/me/invitations//", + UserWorkspaceInvitationEndpoint.as_view( + { + "get": "retrieve", + } + ), + name="user-workspace-invitation", + ), + # user join workspace + # User Graphs + path( + "users/me/workspaces//activity-graph/", + UserActivityGraphEndpoint.as_view(), + name="user-activity-graph", + ), + path( + "users/me/workspaces//issues-completed-graph/", + UserIssueCompletedGraphEndpoint.as_view(), + name="completed-graph", + ), + path( + "users/me/workspaces//dashboard/", + UserWorkspaceDashboardEndpoint.as_view(), + name="user-workspace-dashboard", + ), + ## End User Graph + path( + "users/me/invitations/workspaces///join/", + JoinWorkspaceEndpoint.as_view(), + name="user-join-workspace", + ), + # user project invitations + path( + "users/me/invitations/projects/", + UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}), + name="user-project-invitations", + ), +] diff --git a/apiserver/plane/api/urls/views.py b/apiserver/plane/api/urls/views.py new file mode 100644 index 00000000000..560855e8021 --- /dev/null +++ b/apiserver/plane/api/urls/views.py @@ -0,0 +1,85 @@ +from django.urls import path + + +from plane.api.views import ( + IssueViewViewSet, + GlobalViewViewSet, + GlobalViewIssuesViewSet, + IssueViewFavoriteViewSet, +) + + +urlpatterns = [ + path( + "workspaces//projects//views/", + IssueViewViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-view", + ), + path( + "workspaces//projects//views//", + IssueViewViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-view", + ), + path( + "workspaces//views/", + GlobalViewViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="global-view", + ), + path( + "workspaces//views//", + GlobalViewViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="global-view", + ), + path( + "workspaces//issues/", + GlobalViewIssuesViewSet.as_view( + { + "get": "list", + } + ), + name="global-view-issues", + ), + path( + "workspaces//projects//user-favorite-views/", + IssueViewFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-view", + ), + path( + "workspaces//projects//user-favorite-views//", + IssueViewFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-view", + ), +] diff --git a/apiserver/plane/api/urls/workspace.py b/apiserver/plane/api/urls/workspace.py new file mode 100644 index 00000000000..f26730833cd --- /dev/null +++ b/apiserver/plane/api/urls/workspace.py @@ -0,0 +1,176 @@ +from django.urls import path + + +from plane.api.views import ( + WorkSpaceViewSet, + InviteWorkspaceEndpoint, + WorkSpaceMemberViewSet, + WorkspaceInvitationsViewset, + WorkspaceMemberUserEndpoint, + WorkspaceMemberUserViewsEndpoint, + WorkSpaceAvailabilityCheckEndpoint, + TeamMemberViewSet, + UserLastProjectWithWorkspaceEndpoint, + WorkspaceThemeViewSet, + WorkspaceUserProfileStatsEndpoint, + WorkspaceUserActivityEndpoint, + WorkspaceUserProfileEndpoint, + WorkspaceUserProfileIssuesEndpoint, + WorkspaceLabelsEndpoint, + LeaveWorkspaceEndpoint, +) + + +urlpatterns = [ + path( + "workspace-slug-check/", + WorkSpaceAvailabilityCheckEndpoint.as_view(), + name="workspace-availability", + ), + path( + "workspaces/", + WorkSpaceViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="workspace", + ), + path( + "workspaces//", + WorkSpaceViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="workspace", + ), + path( + "workspaces//invite/", + InviteWorkspaceEndpoint.as_view(), + name="invite-workspace", + ), + path( + "workspaces//invitations/", + WorkspaceInvitationsViewset.as_view({"get": "list"}), + name="workspace-invitations", + ), + path( + "workspaces//invitations//", + WorkspaceInvitationsViewset.as_view( + { + "delete": "destroy", + "get": "retrieve", + } + ), + name="workspace-invitations", + ), + path( + "workspaces//members/", + WorkSpaceMemberViewSet.as_view({"get": "list"}), + name="workspace-member", + ), + path( + "workspaces//members//", + WorkSpaceMemberViewSet.as_view( + { + "patch": "partial_update", + "delete": "destroy", + "get": "retrieve", + } + ), + name="workspace-member", + ), + path( + "workspaces//teams/", + TeamMemberViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="workspace-team-members", + ), + path( + "workspaces//teams//", + TeamMemberViewSet.as_view( + { + "put": "update", + "patch": "partial_update", + "delete": "destroy", + "get": "retrieve", + } + ), + name="workspace-team-members", + ), + path( + "users/last-visited-workspace/", + UserLastProjectWithWorkspaceEndpoint.as_view(), + name="workspace-project-details", + ), + path( + "workspaces//workspace-members/me/", + WorkspaceMemberUserEndpoint.as_view(), + name="workspace-member-details", + ), + path( + "workspaces//workspace-views/", + WorkspaceMemberUserViewsEndpoint.as_view(), + name="workspace-member-views-details", + ), + path( + "workspaces//workspace-themes/", + WorkspaceThemeViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="workspace-themes", + ), + path( + "workspaces//workspace-themes//", + WorkspaceThemeViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="workspace-themes", + ), + path( + "workspaces//user-stats//", + WorkspaceUserProfileStatsEndpoint.as_view(), + name="workspace-user-stats", + ), + path( + "workspaces//user-activity//", + WorkspaceUserActivityEndpoint.as_view(), + name="workspace-user-activity", + ), + path( + "workspaces//user-profile//", + WorkspaceUserProfileEndpoint.as_view(), + name="workspace-user-profile-page", + ), + path( + "workspaces//user-issues//", + WorkspaceUserProfileIssuesEndpoint.as_view(), + name="workspace-user-profile-issues", + ), + path( + "workspaces//labels/", + WorkspaceLabelsEndpoint.as_view(), + name="workspace-labels", + ), + path( + "workspaces//members/leave/", + LeaveWorkspaceEndpoint.as_view(), + name="leave-workspace-members", + ), +] diff --git a/apiserver/plane/api/urls_deprecated.py b/apiserver/plane/api/urls_deprecated.py new file mode 100644 index 00000000000..67cc62e4635 --- /dev/null +++ b/apiserver/plane/api/urls_deprecated.py @@ -0,0 +1,1740 @@ +from django.urls import path + +from rest_framework_simplejwt.views import TokenRefreshView + +# Create your urls here. + +from plane.api.views import ( + # Authentication + SignUpEndpoint, + SignInEndpoint, + SignOutEndpoint, + MagicSignInEndpoint, + MagicSignInGenerateEndpoint, + OauthEndpoint, + ## End Authentication + # Auth Extended + ForgotPasswordEndpoint, + VerifyEmailEndpoint, + ResetPasswordEndpoint, + RequestEmailVerificationEndpoint, + ChangePasswordEndpoint, + ## End Auth Extender + # User + UserEndpoint, + UpdateUserOnBoardedEndpoint, + UpdateUserTourCompletedEndpoint, + UserActivityEndpoint, + ## End User + # Workspaces + WorkSpaceViewSet, + UserWorkSpacesEndpoint, + InviteWorkspaceEndpoint, + JoinWorkspaceEndpoint, + WorkSpaceMemberViewSet, + WorkspaceMembersEndpoint, + WorkspaceInvitationsViewset, + UserWorkspaceInvitationsEndpoint, + WorkspaceMemberUserEndpoint, + WorkspaceMemberUserViewsEndpoint, + WorkSpaceAvailabilityCheckEndpoint, + TeamMemberViewSet, + AddTeamToProjectEndpoint, + UserLastProjectWithWorkspaceEndpoint, + UserWorkspaceInvitationEndpoint, + UserActivityGraphEndpoint, + UserIssueCompletedGraphEndpoint, + UserWorkspaceDashboardEndpoint, + WorkspaceThemeViewSet, + WorkspaceUserProfileStatsEndpoint, + WorkspaceUserActivityEndpoint, + WorkspaceUserProfileEndpoint, + WorkspaceUserProfileIssuesEndpoint, + WorkspaceLabelsEndpoint, + LeaveWorkspaceEndpoint, + ## End Workspaces + # File Assets + FileAssetEndpoint, + UserAssetsEndpoint, + ## End File Assets + # Projects + ProjectViewSet, + InviteProjectEndpoint, + ProjectMemberViewSet, + ProjectMemberEndpoint, + ProjectMemberInvitationsViewset, + ProjectMemberUserEndpoint, + AddMemberToProjectEndpoint, + ProjectJoinEndpoint, + UserProjectInvitationsViewset, + ProjectIdentifierEndpoint, + ProjectFavoritesViewSet, + LeaveProjectEndpoint, + ProjectPublicCoverImagesEndpoint, + ## End Projects + # Issues + IssueViewSet, + WorkSpaceIssuesEndpoint, + IssueActivityEndpoint, + IssueCommentViewSet, + UserWorkSpaceIssues, + BulkDeleteIssuesEndpoint, + BulkImportIssuesEndpoint, + ProjectUserViewsEndpoint, + IssueUserDisplayPropertyEndpoint, + LabelViewSet, + SubIssuesEndpoint, + IssueLinkViewSet, + BulkCreateIssueLabelsEndpoint, + IssueAttachmentEndpoint, + IssueArchiveViewSet, + IssueSubscriberViewSet, + IssueCommentPublicViewSet, + IssueReactionViewSet, + IssueRelationViewSet, + CommentReactionViewSet, + IssueDraftViewSet, + ## End Issues + # States + StateViewSet, + ## End States + # Estimates + ProjectEstimatePointEndpoint, + BulkEstimatePointEndpoint, + ## End Estimates + # Views + GlobalViewViewSet, + GlobalViewIssuesViewSet, + IssueViewViewSet, + IssueViewFavoriteViewSet, + ## End Views + # Cycles + CycleViewSet, + CycleIssueViewSet, + CycleDateCheckEndpoint, + CycleFavoriteViewSet, + TransferCycleIssueEndpoint, + ## End Cycles + # Modules + ModuleViewSet, + ModuleIssueViewSet, + ModuleFavoriteViewSet, + ModuleLinkViewSet, + BulkImportModulesEndpoint, + ## End Modules + # Pages + PageViewSet, + PageBlockViewSet, + PageFavoriteViewSet, + CreateIssueFromPageBlockEndpoint, + ## End Pages + # Api Tokens + ApiTokenEndpoint, + ## End Api Tokens + # Integrations + IntegrationViewSet, + WorkspaceIntegrationViewSet, + GithubRepositoriesEndpoint, + GithubRepositorySyncViewSet, + GithubIssueSyncViewSet, + GithubCommentSyncViewSet, + BulkCreateGithubIssueSyncEndpoint, + SlackProjectSyncViewSet, + ## End Integrations + # Importer + ServiceIssueImportSummaryEndpoint, + ImportServiceEndpoint, + UpdateServiceImportStatusEndpoint, + ## End importer + # Search + GlobalSearchEndpoint, + IssueSearchEndpoint, + ## End Search + # External + GPTIntegrationEndpoint, + ReleaseNotesEndpoint, + UnsplashEndpoint, + ## End External + # Inbox + InboxViewSet, + InboxIssueViewSet, + ## End Inbox + # Analytics + AnalyticsEndpoint, + AnalyticViewViewset, + SavedAnalyticEndpoint, + ExportAnalyticsEndpoint, + DefaultAnalyticsEndpoint, + ## End Analytics + # Notification + NotificationViewSet, + UnreadNotificationEndpoint, + MarkAllReadNotificationViewSet, + ## End Notification + # Public Boards + ProjectDeployBoardViewSet, + ProjectIssuesPublicEndpoint, + ProjectDeployBoardPublicSettingsEndpoint, + IssueReactionPublicViewSet, + CommentReactionPublicViewSet, + InboxIssuePublicViewSet, + IssueVotePublicViewSet, + WorkspaceProjectDeployBoardEndpoint, + IssueRetrievePublicEndpoint, + ## End Public Boards + ## Exporter + ExportIssuesEndpoint, + ## End Exporter + # Configuration + ConfigurationEndpoint, + ## End Configuration +) + + +#TODO: Delete this file +# This url file has been deprecated use apiserver/plane/urls folder to create new urls + +urlpatterns = [ + # Social Auth + path("social-auth/", OauthEndpoint.as_view(), name="oauth"), + # Auth + path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"), + path("sign-in/", SignInEndpoint.as_view(), name="sign-in"), + path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), + # Magic Sign In/Up + path( + "magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate" + ), + path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"), + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + # Email verification + path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"), + path( + "request-email-verify/", + RequestEmailVerificationEndpoint.as_view(), + name="request-reset-email", + ), + # Password Manipulation + path( + "reset-password///", + ResetPasswordEndpoint.as_view(), + name="password-reset", + ), + path( + "forgot-password/", + ForgotPasswordEndpoint.as_view(), + name="forgot-password", + ), + # User Profile + path( + "users/me/", + UserEndpoint.as_view( + {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} + ), + name="users", + ), + path( + "users/me/settings/", + UserEndpoint.as_view( + { + "get": "retrieve_user_settings", + } + ), + name="users", + ), + path( + "users/me/change-password/", + ChangePasswordEndpoint.as_view(), + name="change-password", + ), + path( + "users/me/onboard/", + UpdateUserOnBoardedEndpoint.as_view(), + name="user-onboard", + ), + path( + "users/me/tour-completed/", + UpdateUserTourCompletedEndpoint.as_view(), + name="user-tour", + ), + path( + "users/workspaces//activities/", + UserActivityEndpoint.as_view(), + name="user-activities", + ), + # user workspaces + path( + "users/me/workspaces/", + UserWorkSpacesEndpoint.as_view(), + name="user-workspace", + ), + # user workspace invitations + path( + "users/me/invitations/workspaces/", + UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}), + name="user-workspace-invitations", + ), + # user workspace invitation + path( + "users/me/invitations//", + UserWorkspaceInvitationEndpoint.as_view( + { + "get": "retrieve", + } + ), + name="workspace", + ), + # user join workspace + # User Graphs + path( + "users/me/workspaces//activity-graph/", + UserActivityGraphEndpoint.as_view(), + name="user-activity-graph", + ), + path( + "users/me/workspaces//issues-completed-graph/", + UserIssueCompletedGraphEndpoint.as_view(), + name="completed-graph", + ), + path( + "users/me/workspaces//dashboard/", + UserWorkspaceDashboardEndpoint.as_view(), + name="user-workspace-dashboard", + ), + ## User Graph + path( + "users/me/invitations/workspaces///join/", + JoinWorkspaceEndpoint.as_view(), + name="user-join-workspace", + ), + # user project invitations + path( + "users/me/invitations/projects/", + UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}), + name="user-project-invitaions", + ), + ## Workspaces ## + path( + "workspace-slug-check/", + WorkSpaceAvailabilityCheckEndpoint.as_view(), + name="workspace-availability", + ), + path( + "workspaces/", + WorkSpaceViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="workspace", + ), + path( + "workspaces//", + WorkSpaceViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="workspace", + ), + path( + "workspaces//invite/", + InviteWorkspaceEndpoint.as_view(), + name="workspace", + ), + path( + "workspaces//invitations/", + WorkspaceInvitationsViewset.as_view({"get": "list"}), + name="workspace", + ), + path( + "workspaces//invitations//", + WorkspaceInvitationsViewset.as_view( + { + "delete": "destroy", + "get": "retrieve", + } + ), + name="workspace", + ), + path( + "workspaces//members/", + WorkSpaceMemberViewSet.as_view({"get": "list"}), + name="workspace", + ), + path( + "workspaces//members//", + WorkSpaceMemberViewSet.as_view( + { + "patch": "partial_update", + "delete": "destroy", + "get": "retrieve", + } + ), + name="workspace", + ), + path( + "workspaces//workspace-members/", + WorkspaceMembersEndpoint.as_view(), + name="workspace-members", + ), + path( + "workspaces//teams/", + TeamMemberViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="workspace", + ), + path( + "workspaces//teams//", + TeamMemberViewSet.as_view( + { + "put": "update", + "patch": "partial_update", + "delete": "destroy", + "get": "retrieve", + } + ), + name="workspace", + ), + path( + "users/last-visited-workspace/", + UserLastProjectWithWorkspaceEndpoint.as_view(), + name="workspace-project-details", + ), + path( + "workspaces//workspace-members/me/", + WorkspaceMemberUserEndpoint.as_view(), + name="workspace-member-details", + ), + path( + "workspaces//workspace-views/", + WorkspaceMemberUserViewsEndpoint.as_view(), + name="workspace-member-details", + ), + path( + "workspaces//workspace-themes/", + WorkspaceThemeViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="workspace-themes", + ), + path( + "workspaces//workspace-themes//", + WorkspaceThemeViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="workspace-themes", + ), + path( + "workspaces//user-stats//", + WorkspaceUserProfileStatsEndpoint.as_view(), + name="workspace-user-stats", + ), + path( + "workspaces//user-activity//", + WorkspaceUserActivityEndpoint.as_view(), + name="workspace-user-activity", + ), + path( + "workspaces//user-profile//", + WorkspaceUserProfileEndpoint.as_view(), + name="workspace-user-profile-page", + ), + path( + "workspaces//user-issues//", + WorkspaceUserProfileIssuesEndpoint.as_view(), + name="workspace-user-profile-issues", + ), + path( + "workspaces//labels/", + WorkspaceLabelsEndpoint.as_view(), + name="workspace-labels", + ), + path( + "workspaces//members/leave/", + LeaveWorkspaceEndpoint.as_view(), + name="workspace-labels", + ), + ## End Workspaces ## + # Projects + path( + "workspaces//projects/", + ProjectViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project", + ), + path( + "workspaces//projects//", + ProjectViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project", + ), + path( + "workspaces//project-identifiers/", + ProjectIdentifierEndpoint.as_view(), + name="project-identifiers", + ), + path( + "workspaces//projects//invite/", + InviteProjectEndpoint.as_view(), + name="project", + ), + path( + "workspaces//projects//members/", + ProjectMemberViewSet.as_view({"get": "list"}), + name="project", + ), + path( + "workspaces//projects//members//", + ProjectMemberViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project", + ), + path( + "workspaces//projects//project-members/", + ProjectMemberEndpoint.as_view(), + name="project", + ), + path( + "workspaces//projects//members/add/", + AddMemberToProjectEndpoint.as_view(), + name="project", + ), + path( + "workspaces//projects/join/", + ProjectJoinEndpoint.as_view(), + name="project", + ), + path( + "workspaces//projects//team-invite/", + AddTeamToProjectEndpoint.as_view(), + name="projects", + ), + path( + "workspaces//projects//invitations/", + ProjectMemberInvitationsViewset.as_view({"get": "list"}), + name="workspace", + ), + path( + "workspaces//projects//invitations//", + ProjectMemberInvitationsViewset.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + name="project", + ), + path( + "workspaces//projects//project-views/", + ProjectUserViewsEndpoint.as_view(), + name="project-view", + ), + path( + "workspaces//projects//project-members/me/", + ProjectMemberUserEndpoint.as_view(), + name="project-view", + ), + path( + "workspaces//user-favorite-projects/", + ProjectFavoritesViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project", + ), + path( + "workspaces//user-favorite-projects//", + ProjectFavoritesViewSet.as_view( + { + "delete": "destroy", + } + ), + name="project", + ), + path( + "workspaces//projects//members/leave/", + LeaveProjectEndpoint.as_view(), + name="project", + ), + path( + "project-covers/", + ProjectPublicCoverImagesEndpoint.as_view(), + name="project-covers", + ), + # End Projects + # States + path( + "workspaces//projects//states/", + StateViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-states", + ), + path( + "workspaces//projects//states//", + StateViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-state", + ), + # End States ## + # Estimates + path( + "workspaces//projects//project-estimates/", + ProjectEstimatePointEndpoint.as_view(), + name="project-estimate-points", + ), + path( + "workspaces//projects//estimates/", + BulkEstimatePointEndpoint.as_view( + { + "get": "list", + "post": "create", + } + ), + name="bulk-create-estimate-points", + ), + path( + "workspaces//projects//estimates//", + BulkEstimatePointEndpoint.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="bulk-create-estimate-points", + ), + # End Estimates ## + # Views + path( + "workspaces//projects//views/", + IssueViewViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-view", + ), + path( + "workspaces//projects//views//", + IssueViewViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-view", + ), + path( + "workspaces//views/", + GlobalViewViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="global-view", + ), + path( + "workspaces//views//", + GlobalViewViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="global-view", + ), + path( + "workspaces//issues/", + GlobalViewIssuesViewSet.as_view( + { + "get": "list", + } + ), + name="global-view-issues", + ), + path( + "workspaces//projects//user-favorite-views/", + IssueViewFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-view", + ), + path( + "workspaces//projects//user-favorite-views//", + IssueViewFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-view", + ), + ## End Views + ## Cycles + path( + "workspaces//projects//cycles/", + CycleViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-cycle", + ), + path( + "workspaces//projects//cycles//", + CycleViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-cycle", + ), + path( + "workspaces//projects//cycles//cycle-issues/", + CycleIssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-cycle", + ), + path( + "workspaces//projects//cycles//cycle-issues//", + CycleIssueViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-cycle", + ), + path( + "workspaces//projects//cycles/date-check/", + CycleDateCheckEndpoint.as_view(), + name="project-cycle", + ), + path( + "workspaces//projects//user-favorite-cycles/", + CycleFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-cycle", + ), + path( + "workspaces//projects//user-favorite-cycles//", + CycleFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-cycle", + ), + path( + "workspaces//projects//cycles//transfer-issues/", + TransferCycleIssueEndpoint.as_view(), + name="transfer-issues", + ), + ## End Cycles + # Issue + path( + "workspaces//projects//issues/", + IssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue", + ), + path( + "workspaces//projects//issues//", + IssueViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue", + ), + path( + "workspaces//projects//issue-labels/", + LabelViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-labels", + ), + path( + "workspaces//projects//issue-labels//", + LabelViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-labels", + ), + path( + "workspaces//projects//bulk-create-labels/", + BulkCreateIssueLabelsEndpoint.as_view(), + name="project-bulk-labels", + ), + path( + "workspaces//projects//bulk-delete-issues/", + BulkDeleteIssuesEndpoint.as_view(), + name="project-issues-bulk", + ), + path( + "workspaces//projects//bulk-import-issues//", + BulkImportIssuesEndpoint.as_view(), + name="project-issues-bulk", + ), + path( + "workspaces//my-issues/", + UserWorkSpaceIssues.as_view(), + name="workspace-issues", + ), + path( + "workspaces//projects//issues//sub-issues/", + SubIssuesEndpoint.as_view(), + name="sub-issues", + ), + path( + "workspaces//projects//issues//issue-links/", + IssueLinkViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-links", + ), + path( + "workspaces//projects//issues//issue-links//", + IssueLinkViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-links", + ), + path( + "workspaces//projects//issues//issue-attachments/", + IssueAttachmentEndpoint.as_view(), + name="project-issue-attachments", + ), + path( + "workspaces//projects//issues//issue-attachments//", + IssueAttachmentEndpoint.as_view(), + name="project-issue-attachments", + ), + path( + "workspaces//export-issues/", + ExportIssuesEndpoint.as_view(), + name="export-issues", + ), + ## End Issues + ## Issue Activity + path( + "workspaces//projects//issues//history/", + IssueActivityEndpoint.as_view(), + name="project-issue-history", + ), + ## Issue Activity + ## IssueComments + path( + "workspaces//projects//issues//comments/", + IssueCommentViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-comment", + ), + path( + "workspaces//projects//issues//comments//", + IssueCommentViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-comment", + ), + ## End IssueComments + # Issue Subscribers + path( + "workspaces//projects//issues//issue-subscribers/", + IssueSubscriberViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-subscribers", + ), + path( + "workspaces//projects//issues//issue-subscribers//", + IssueSubscriberViewSet.as_view({"delete": "destroy"}), + name="project-issue-subscribers", + ), + path( + "workspaces//projects//issues//subscribe/", + IssueSubscriberViewSet.as_view( + { + "get": "subscription_status", + "post": "subscribe", + "delete": "unsubscribe", + } + ), + name="project-issue-subscribers", + ), + ## End Issue Subscribers + # Issue Reactions + path( + "workspaces//projects//issues//reactions/", + IssueReactionViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-reactions", + ), + path( + "workspaces//projects//issues//reactions//", + IssueReactionViewSet.as_view( + { + "delete": "destroy", + } + ), + name="project-issue-reactions", + ), + ## End Issue Reactions + # Comment Reactions + path( + "workspaces//projects//comments//reactions/", + CommentReactionViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-comment-reactions", + ), + path( + "workspaces//projects//comments//reactions//", + CommentReactionViewSet.as_view( + { + "delete": "destroy", + } + ), + name="project-issue-comment-reactions", + ), + ## End Comment Reactions + ## IssueProperty + path( + "workspaces//projects//issue-display-properties/", + IssueUserDisplayPropertyEndpoint.as_view(), + name="project-issue-display-properties", + ), + ## IssueProperty Ebd + ## Issue Archives + path( + "workspaces//projects//archived-issues/", + IssueArchiveViewSet.as_view( + { + "get": "list", + } + ), + name="project-issue-archive", + ), + path( + "workspaces//projects//archived-issues//", + IssueArchiveViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + name="project-issue-archive", + ), + path( + "workspaces//projects//unarchive//", + IssueArchiveViewSet.as_view( + { + "post": "unarchive", + } + ), + name="project-issue-archive", + ), + ## End Issue Archives + ## Issue Relation + path( + "workspaces//projects//issues//issue-relation/", + IssueRelationViewSet.as_view( + { + "post": "create", + } + ), + name="issue-relation", + ), + path( + "workspaces//projects//issues//issue-relation//", + IssueRelationViewSet.as_view( + { + "delete": "destroy", + } + ), + name="issue-relation", + ), + ## End Issue Relation + ## Issue Drafts + path( + "workspaces//projects//issue-drafts/", + IssueDraftViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-draft", + ), + path( + "workspaces//projects//issue-drafts//", + IssueDraftViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-draft", + ), + ## End Issue Drafts + ## File Assets + path( + "workspaces//file-assets/", + FileAssetEndpoint.as_view(), + name="file-assets", + ), + path( + "workspaces/file-assets///", + FileAssetEndpoint.as_view(), + name="file-assets", + ), + path( + "users/file-assets/", + UserAssetsEndpoint.as_view(), + name="user-file-assets", + ), + path( + "users/file-assets//", + UserAssetsEndpoint.as_view(), + name="user-file-assets", + ), + ## End File Assets + ## Modules + path( + "workspaces//projects//modules/", + ModuleViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-modules", + ), + path( + "workspaces//projects//modules//", + ModuleViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-modules", + ), + path( + "workspaces//projects//modules//module-issues/", + ModuleIssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-module-issues", + ), + path( + "workspaces//projects//modules//module-issues//", + ModuleIssueViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-module-issues", + ), + path( + "workspaces//projects//modules//module-links/", + ModuleLinkViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-module-links", + ), + path( + "workspaces//projects//modules//module-links//", + ModuleLinkViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-module-links", + ), + path( + "workspaces//projects//user-favorite-modules/", + ModuleFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-module", + ), + path( + "workspaces//projects//user-favorite-modules//", + ModuleFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-module", + ), + path( + "workspaces//projects//bulk-import-modules//", + BulkImportModulesEndpoint.as_view(), + name="bulk-modules-create", + ), + ## End Modules + # Pages + path( + "workspaces//projects//pages/", + PageViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-pages", + ), + path( + "workspaces//projects//pages//", + PageViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-pages", + ), + path( + "workspaces//projects//pages//page-blocks/", + PageBlockViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-page-blocks", + ), + path( + "workspaces//projects//pages//page-blocks//", + PageBlockViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-page-blocks", + ), + path( + "workspaces//projects//user-favorite-pages/", + PageFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-pages", + ), + path( + "workspaces//projects//user-favorite-pages//", + PageFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-pages", + ), + path( + "workspaces//projects//pages//page-blocks//issues/", + CreateIssueFromPageBlockEndpoint.as_view(), + name="page-block-issues", + ), + ## End Pages + # API Tokens + path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), + path("api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens"), + ## End API Tokens + # Integrations + path( + "integrations/", + IntegrationViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="integrations", + ), + path( + "integrations//", + IntegrationViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="integrations", + ), + path( + "workspaces//workspace-integrations/", + WorkspaceIntegrationViewSet.as_view( + { + "get": "list", + } + ), + name="workspace-integrations", + ), + path( + "workspaces//workspace-integrations//", + WorkspaceIntegrationViewSet.as_view( + { + "post": "create", + } + ), + name="workspace-integrations", + ), + path( + "workspaces//workspace-integrations//provider/", + WorkspaceIntegrationViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + name="workspace-integrations", + ), + # Github Integrations + path( + "workspaces//workspace-integrations//github-repositories/", + GithubRepositoriesEndpoint.as_view(), + ), + path( + "workspaces//projects//workspace-integrations//github-repository-sync/", + GithubRepositorySyncViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + ), + path( + "workspaces//projects//workspace-integrations//github-repository-sync//", + GithubRepositorySyncViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync/", + GithubIssueSyncViewSet.as_view( + { + "post": "create", + "get": "list", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//bulk-create-github-issue-sync/", + BulkCreateGithubIssueSyncEndpoint.as_view(), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync//", + GithubIssueSyncViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync/", + GithubCommentSyncViewSet.as_view( + { + "post": "create", + "get": "list", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync//", + GithubCommentSyncViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + ), + ## End Github Integrations + # Slack Integration + path( + "workspaces//projects//workspace-integrations//project-slack-sync/", + SlackProjectSyncViewSet.as_view( + { + "post": "create", + "get": "list", + } + ), + ), + path( + "workspaces//projects//workspace-integrations//project-slack-sync//", + SlackProjectSyncViewSet.as_view( + { + "delete": "destroy", + "get": "retrieve", + } + ), + ), + ## End Slack Integration + ## End Integrations + # Importer + path( + "workspaces//importers//", + ServiceIssueImportSummaryEndpoint.as_view(), + name="importer", + ), + path( + "workspaces//projects/importers//", + ImportServiceEndpoint.as_view(), + name="importer", + ), + path( + "workspaces//importers/", + ImportServiceEndpoint.as_view(), + name="importer", + ), + path( + "workspaces//importers///", + ImportServiceEndpoint.as_view(), + name="importer", + ), + path( + "workspaces//projects//service//importers//", + UpdateServiceImportStatusEndpoint.as_view(), + name="importer", + ), + ## End Importer + # Search + path( + "workspaces//search/", + GlobalSearchEndpoint.as_view(), + name="global-search", + ), + path( + "workspaces//projects//search-issues/", + IssueSearchEndpoint.as_view(), + name="project-issue-search", + ), + ## End Search + # External + path( + "workspaces//projects//ai-assistant/", + GPTIntegrationEndpoint.as_view(), + name="importer", + ), + path( + "release-notes/", + ReleaseNotesEndpoint.as_view(), + name="release-notes", + ), + path( + "unsplash/", + UnsplashEndpoint.as_view(), + name="release-notes", + ), + ## End External + # Inbox + path( + "workspaces//projects//inboxes/", + InboxViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox", + ), + path( + "workspaces//projects//inboxes//", + InboxViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox", + ), + path( + "workspaces//projects//inboxes//inbox-issues/", + InboxIssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox-issue", + ), + path( + "workspaces//projects//inboxes//inbox-issues//", + InboxIssueViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox-issue", + ), + ## End Inbox + # Analytics + path( + "workspaces//analytics/", + AnalyticsEndpoint.as_view(), + name="plane-analytics", + ), + path( + "workspaces//analytic-view/", + AnalyticViewViewset.as_view({"get": "list", "post": "create"}), + name="analytic-view", + ), + path( + "workspaces//analytic-view//", + AnalyticViewViewset.as_view( + {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} + ), + name="analytic-view", + ), + path( + "workspaces//saved-analytic-view//", + SavedAnalyticEndpoint.as_view(), + name="saved-analytic-view", + ), + path( + "workspaces//export-analytics/", + ExportAnalyticsEndpoint.as_view(), + name="export-analytics", + ), + path( + "workspaces//default-analytics/", + DefaultAnalyticsEndpoint.as_view(), + name="default-analytics", + ), + ## End Analytics + # Notification + path( + "workspaces//users/notifications/", + NotificationViewSet.as_view( + { + "get": "list", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications//", + NotificationViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications//read/", + NotificationViewSet.as_view( + { + "post": "mark_read", + "delete": "mark_unread", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications//archive/", + NotificationViewSet.as_view( + { + "post": "archive", + "delete": "unarchive", + } + ), + name="notifications", + ), + path( + "workspaces//users/notifications/unread/", + UnreadNotificationEndpoint.as_view(), + name="unread-notifications", + ), + path( + "workspaces//users/notifications/mark-all-read/", + MarkAllReadNotificationViewSet.as_view( + { + "post": "create", + } + ), + name="mark-all-read-notifications", + ), + ## End Notification + # Public Boards + path( + "workspaces//projects//project-deploy-boards/", + ProjectDeployBoardViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-deploy-board", + ), + path( + "workspaces//projects//project-deploy-boards//", + ProjectDeployBoardViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-deploy-board", + ), + path( + "public/workspaces//project-boards//settings/", + ProjectDeployBoardPublicSettingsEndpoint.as_view(), + name="project-deploy-board-settings", + ), + path( + "public/workspaces//project-boards//issues/", + ProjectIssuesPublicEndpoint.as_view(), + name="project-deploy-board", + ), + path( + "public/workspaces//project-boards//issues//", + IssueRetrievePublicEndpoint.as_view(), + name="workspace-project-boards", + ), + path( + "public/workspaces//project-boards//issues//comments/", + IssueCommentPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="issue-comments-project-board", + ), + path( + "public/workspaces//project-boards//issues//comments//", + IssueCommentPublicViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="issue-comments-project-board", + ), + path( + "public/workspaces//project-boards//issues//reactions/", + IssueReactionPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="issue-reactions-project-board", + ), + path( + "public/workspaces//project-boards//issues//reactions//", + IssueReactionPublicViewSet.as_view( + { + "delete": "destroy", + } + ), + name="issue-reactions-project-board", + ), + path( + "public/workspaces//project-boards//comments//reactions/", + CommentReactionPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="comment-reactions-project-board", + ), + path( + "public/workspaces//project-boards//comments//reactions//", + CommentReactionPublicViewSet.as_view( + { + "delete": "destroy", + } + ), + name="comment-reactions-project-board", + ), + path( + "public/workspaces//project-boards//inboxes//inbox-issues/", + InboxIssuePublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox-issue", + ), + path( + "public/workspaces//project-boards//inboxes//inbox-issues//", + InboxIssuePublicViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox-issue", + ), + path( + "public/workspaces//project-boards//issues//votes/", + IssueVotePublicViewSet.as_view( + { + "get": "list", + "post": "create", + "delete": "destroy", + } + ), + name="issue-vote-project-board", + ), + path( + "public/workspaces//project-boards/", + WorkspaceProjectDeployBoardEndpoint.as_view(), + name="workspace-project-boards", + ), + ## End Public Boards + # Configuration + path( + "configs/", + ConfigurationEndpoint.as_view(), + name="configuration", + ), + ## End Configuration +] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index b697741ae2a..8f4b2fb9d98 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -7,15 +7,15 @@ ProjectMemberInvitationsViewset, ProjectMemberInviteDetailViewSet, ProjectIdentifierEndpoint, - AddMemberToProjectEndpoint, ProjectJoinEndpoint, ProjectUserViewsEndpoint, ProjectMemberUserEndpoint, ProjectFavoritesViewSet, ProjectDeployBoardViewSet, ProjectDeployBoardPublicSettingsEndpoint, - ProjectMemberEndpoint, WorkspaceProjectDeployBoardEndpoint, + LeaveProjectEndpoint, + ProjectPublicCoverImagesEndpoint, ) from .user import ( UserEndpoint, @@ -51,10 +51,10 @@ WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, - WorkspaceMembersEndpoint, + LeaveWorkspaceEndpoint, ) from .state import StateViewSet -from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet +from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet from .cycle import ( CycleViewSet, CycleIssueViewSet, @@ -68,7 +68,7 @@ WorkSpaceIssuesEndpoint, IssueActivityEndpoint, IssueCommentViewSet, - IssuePropertyViewSet, + IssueUserDisplayPropertyEndpoint, LabelViewSet, BulkDeleteIssuesEndpoint, UserWorkSpaceIssues, @@ -84,8 +84,10 @@ IssueReactionPublicViewSet, CommentReactionPublicViewSet, IssueVotePublicViewSet, + IssueRelationViewSet, IssueRetrievePublicEndpoint, ProjectIssuesPublicEndpoint, + IssueDraftViewSet, ) from .auth_extended import ( @@ -143,16 +145,13 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint -from .gpt import GPTIntegrationEndpoint +from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint from .estimate import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, ) - -from .release import ReleaseNotesEndpoint - from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet from .analytic import ( @@ -165,6 +164,6 @@ from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet -from .exporter import ( - ExportIssuesEndpoint, -) \ No newline at end of file +from .exporter import ExportIssuesEndpoint + +from .config import ConfigurationEndpoint diff --git a/apiserver/plane/api/views/analytic.py b/apiserver/plane/api/views/analytic.py index feb766b46df..c29a4b692b0 100644 --- a/apiserver/plane/api/views/analytic.py +++ b/apiserver/plane/api/views/analytic.py @@ -1,10 +1,5 @@ # Django imports -from django.db.models import ( - Count, - Sum, - F, - Q -) +from django.db.models import Count, Sum, F, Q from django.db.models.functions import ExtractMonth # Third party imports @@ -28,82 +23,156 @@ class AnalyticsEndpoint(BaseAPIView): ] def get(self, request, slug): - try: - x_axis = request.GET.get("x_axis", False) - y_axis = request.GET.get("y_axis", False) - - if not x_axis or not y_axis: - return Response( - {"error": "x-axis and y-axis dimensions are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + x_axis = request.GET.get("x_axis", False) + y_axis = request.GET.get("y_axis", False) + segment = request.GET.get("segment", False) + + valid_xaxis_segment = [ + "state_id", + "state__group", + "labels__id", + "assignees__id", + "estimate_point", + "issue_cycle__cycle_id", + "issue_module__module_id", + "priority", + "start_date", + "target_date", + "created_at", + "completed_at", + ] + + valid_yaxis = [ + "issue_count", + "estimate", + ] + + # Check for x-axis and y-axis as thery are required parameters + if ( + not x_axis + or not y_axis + or not x_axis in valid_xaxis_segment + or not y_axis in valid_yaxis + ): + return Response( + { + "error": "x-axis and y-axis dimensions are required and the values should be valid" + }, + status=status.HTTP_400_BAD_REQUEST, + ) - segment = request.GET.get("segment", False) - filters = issue_filters(request.GET, "GET") + # If segment is present it cannot be same as x-axis + if segment and (segment not in valid_xaxis_segment or x_axis == segment): + return Response( + { + "error": "Both segment and x axis cannot be same and segment should be valid" + }, + status=status.HTTP_400_BAD_REQUEST, + ) - queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters) + # Additional filters that need to be applied + filters = issue_filters(request.GET, "GET") - total_issues = queryset.count() - distribution = build_graph_plot( - queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment - ) + # Get the issues for the workspace with the additional filters applied + queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters) - colors = dict() - if x_axis in ["state__name", "state__group"] or segment in [ - "state__name", - "state__group", - ]: - if x_axis in ["state__name", "state__group"]: - key = "name" if x_axis == "state__name" else "group" - else: - key = "name" if segment == "state__name" else "group" - - colors = ( - State.objects.filter( - ~Q(name="Triage"), - workspace__slug=slug, project_id__in=filters.get("project__in") - ).values(key, "color") - if filters.get("project__in", False) - else State.objects.filter(~Q(name="Triage"), workspace__slug=slug).values(key, "color") - ) + # Get the total issue count + total_issues = queryset.count() + + # Build the graph payload + distribution = build_graph_plot( + queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment + ) - if x_axis in ["labels__name"] or segment in ["labels__name"]: - colors = ( - Label.objects.filter( - workspace__slug=slug, project_id__in=filters.get("project__in") - ).values("name", "color") - if filters.get("project__in", False) - else Label.objects.filter(workspace__slug=slug).values( - "name", "color" - ) + state_details = {} + if x_axis in ["state_id"] or segment in ["state_id"]: + state_details = ( + Issue.issue_objects.filter( + workspace__slug=slug, + **filters, ) + .distinct("state_id") + .order_by("state_id") + .values("state_id", "state__name", "state__color") + ) - assignee_details = {} - if x_axis in ["assignees__id"] or segment in ["assignees__id"]: - assignee_details = ( - Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) - .order_by("assignees__id") - .distinct("assignees__id") - .values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id") + label_details = {} + if x_axis in ["labels__id"] or segment in ["labels__id"]: + label_details = ( + Issue.objects.filter( + workspace__slug=slug, **filters, labels__id__isnull=False ) + .distinct("labels__id") + .order_by("labels__id") + .values("labels__id", "labels__color", "labels__name") + ) + assignee_details = {} + if x_axis in ["assignees__id"] or segment in ["assignees__id"]: + assignee_details = ( + Issue.issue_objects.filter( + workspace__slug=slug, **filters, assignees__avatar__isnull=False + ) + .order_by("assignees__id") + .distinct("assignees__id") + .values( + "assignees__avatar", + "assignees__display_name", + "assignees__first_name", + "assignees__last_name", + "assignees__id", + ) + ) - return Response( - { - "total": total_issues, - "distribution": distribution, - "extras": {"colors": colors, "assignee_details": assignee_details}, - }, - status=status.HTTP_200_OK, + cycle_details = {} + if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]: + cycle_details = ( + Issue.issue_objects.filter( + workspace__slug=slug, + **filters, + issue_cycle__cycle_id__isnull=False, + ) + .distinct("issue_cycle__cycle_id") + .order_by("issue_cycle__cycle_id") + .values( + "issue_cycle__cycle_id", + "issue_cycle__cycle__name", + ) ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + module_details = {} + if x_axis in ["issue_module__module_id"] or segment in [ + "issue_module__module_id" + ]: + module_details = ( + Issue.issue_objects.filter( + workspace__slug=slug, + **filters, + issue_module__module_id__isnull=False, + ) + .distinct("issue_module__module_id") + .order_by("issue_module__module_id") + .values( + "issue_module__module_id", + "issue_module__module__name", + ) ) + return Response( + { + "total": total_issues, + "distribution": distribution, + "extras": { + "state_details": state_details, + "assignee_details": assignee_details, + "label_details": label_details, + "cycle_details": cycle_details, + "module_details": module_details, + }, + }, + status=status.HTTP_200_OK, + ) + class AnalyticViewViewset(BaseViewSet): permission_classes = [ @@ -128,45 +197,30 @@ class SavedAnalyticEndpoint(BaseAPIView): ] def get(self, request, slug, analytic_id): - try: - analytic_view = AnalyticView.objects.get( - pk=analytic_id, workspace__slug=slug - ) - - filter = analytic_view.query - queryset = Issue.issue_objects.filter(**filter) + analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug) - x_axis = analytic_view.query_dict.get("x_axis", False) - y_axis = analytic_view.query_dict.get("y_axis", False) + filter = analytic_view.query + queryset = Issue.issue_objects.filter(**filter) - if not x_axis or not y_axis: - return Response( - {"error": "x-axis and y-axis dimensions are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - segment = request.GET.get("segment", False) - distribution = build_graph_plot( - queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment - ) - total_issues = queryset.count() - return Response( - {"total": total_issues, "distribution": distribution}, - status=status.HTTP_200_OK, - ) + x_axis = analytic_view.query_dict.get("x_axis", False) + y_axis = analytic_view.query_dict.get("y_axis", False) - except AnalyticView.DoesNotExist: + if not x_axis or not y_axis: return Response( - {"error": "Analytic View Does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, + {"error": "x-axis and y-axis dimensions are required"}, status=status.HTTP_400_BAD_REQUEST, ) + segment = request.GET.get("segment", False) + distribution = build_graph_plot( + queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment + ) + total_issues = queryset.count() + return Response( + {"total": total_issues, "distribution": distribution}, + status=status.HTTP_200_OK, + ) + class ExportAnalyticsEndpoint(BaseAPIView): permission_classes = [ @@ -174,33 +228,64 @@ class ExportAnalyticsEndpoint(BaseAPIView): ] def post(self, request, slug): - try: - x_axis = request.data.get("x_axis", False) - y_axis = request.data.get("y_axis", False) - - if not x_axis or not y_axis: - return Response( - {"error": "x-axis and y-axis dimensions are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - analytic_export_task.delay( - email=request.user.email, data=request.data, slug=slug - ) - + x_axis = request.data.get("x_axis", False) + y_axis = request.data.get("y_axis", False) + segment = request.data.get("segment", False) + + valid_xaxis_segment = [ + "state_id", + "state__group", + "labels__id", + "assignees__id", + "estimate_point", + "issue_cycle__cycle_id", + "issue_module__module_id", + "priority", + "start_date", + "target_date", + "created_at", + "completed_at", + ] + + valid_yaxis = [ + "issue_count", + "estimate", + ] + + # Check for x-axis and y-axis as thery are required parameters + if ( + not x_axis + or not y_axis + or not x_axis in valid_xaxis_segment + or not y_axis in valid_yaxis + ): return Response( { - "message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}" + "error": "x-axis and y-axis dimensions are required and the values should be valid" }, - status=status.HTTP_200_OK, + status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) + + # If segment is present it cannot be same as x-axis + if segment and (segment not in valid_xaxis_segment or x_axis == segment): return Response( - {"error": "Something went wrong please try again later"}, + { + "error": "Both segment and x axis cannot be same and segment should be valid" + }, status=status.HTTP_400_BAD_REQUEST, ) + analytic_export_task.delay( + email=request.user.email, data=request.data, slug=slug + ) + + return Response( + { + "message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}" + }, + status=status.HTTP_200_OK, + ) + class DefaultAnalyticsEndpoint(BaseAPIView): permission_classes = [ @@ -208,90 +293,92 @@ class DefaultAnalyticsEndpoint(BaseAPIView): ] def get(self, request, slug): - try: - filters = issue_filters(request.GET, "GET") + filters = issue_filters(request.GET, "GET") + base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters) - queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters) + total_issues = base_issues.count() - total_issues = queryset.count() + state_groups = base_issues.annotate(state_group=F("state__group")) - total_issues_classified = ( - queryset.annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) + total_issues_classified = ( + state_groups.values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) - open_issues = queryset.filter( - state__group__in=["backlog", "unstarted", "started"] - ).count() + open_issues_groups = ["backlog", "unstarted", "started"] + open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups) - open_issues_classified = ( - queryset.filter(state__group__in=["backlog", "unstarted", "started"]) - .annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) + open_issues = open_issues_queryset.count() + open_issues_classified = ( + open_issues_queryset.values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) - issue_completed_month_wise = ( - queryset.filter(completed_at__isnull=False) - .annotate(month=ExtractMonth("completed_at")) - .values("month") - .annotate(count=Count("*")) - .order_by("month") - ) - most_issue_created_user = ( - queryset.exclude(created_by=None) - .values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name", "created_by__id") - .annotate(count=Count("id")) - .order_by("-count") - )[:5] - - most_issue_closed_user = ( - queryset.filter(completed_at__isnull=False, assignees__isnull=False) - .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id") - .annotate(count=Count("id")) - .order_by("-count") - )[:5] - - pending_issue_user = ( - queryset.filter(completed_at__isnull=True) - .values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id") - .annotate(count=Count("id")) - .order_by("-count") - ) + issue_completed_month_wise = ( + base_issues.filter(completed_at__isnull=False) + .annotate(month=ExtractMonth("completed_at")) + .values("month") + .annotate(count=Count("*")) + .order_by("month") + ) - open_estimate_sum = ( - queryset.filter( - state__group__in=["backlog", "unstarted", "started"] - ).aggregate(open_estimate_sum=Sum("estimate_point")) - )["open_estimate_sum"] - print(open_estimate_sum) - - total_estimate_sum = queryset.aggregate( - total_estimate_sum=Sum("estimate_point") - )["total_estimate_sum"] + user_details = [ + "created_by__first_name", + "created_by__last_name", + "created_by__avatar", + "created_by__display_name", + "created_by__id", + ] + + most_issue_created_user = ( + base_issues.exclude(created_by=None) + .values(*user_details) + .annotate(count=Count("id")) + .order_by("-count")[:5] + ) - return Response( - { - "total_issues": total_issues, - "total_issues_classified": total_issues_classified, - "open_issues": open_issues, - "open_issues_classified": open_issues_classified, - "issue_completed_month_wise": issue_completed_month_wise, - "most_issue_created_user": most_issue_created_user, - "most_issue_closed_user": most_issue_closed_user, - "pending_issue_user": pending_issue_user, - "open_estimate_sum": open_estimate_sum, - "total_estimate_sum": total_estimate_sum, - }, - status=status.HTTP_200_OK, - ) + user_assignee_details = [ + "assignees__first_name", + "assignees__last_name", + "assignees__avatar", + "assignees__display_name", + "assignees__id", + ] + + most_issue_closed_user = ( + base_issues.filter(completed_at__isnull=False) + .exclude(assignees=None) + .values(*user_assignee_details) + .annotate(count=Count("id")) + .order_by("-count")[:5] + ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + pending_issue_user = ( + base_issues.filter(completed_at__isnull=True) + .values(*user_assignee_details) + .annotate(count=Count("id")) + .order_by("-count") + ) + + open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("estimate_point"))[ + "sum" + ] + total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))["sum"] + + return Response( + { + "total_issues": total_issues, + "total_issues_classified": total_issues_classified, + "open_issues": open_issues, + "open_issues_classified": open_issues_classified, + "issue_completed_month_wise": issue_completed_month_wise, + "most_issue_created_user": most_issue_created_user, + "most_issue_closed_user": most_issue_closed_user, + "pending_issue_user": pending_issue_user, + "open_estimate_sum": open_estimate_sum, + "total_estimate_sum": total_estimate_sum, + }, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/api/views/api_token.py b/apiserver/plane/api/views/api_token.py index a94ffb45ca2..2253903a9f5 100644 --- a/apiserver/plane/api/views/api_token.py +++ b/apiserver/plane/api/views/api_token.py @@ -14,57 +14,34 @@ class ApiTokenEndpoint(BaseAPIView): def post(self, request): - try: - label = request.data.get("label", str(uuid4().hex)) - workspace = request.data.get("workspace", False) + label = request.data.get("label", str(uuid4().hex)) + workspace = request.data.get("workspace", False) - if not workspace: - return Response( - {"error": "Workspace is required"}, status=status.HTTP_200_OK - ) - - api_token = APIToken.objects.create( - label=label, user=request.user, workspace_id=workspace - ) - - serializer = APITokenSerializer(api_token) - # Token will be only vissible while creating + if not workspace: return Response( - {"api_token": serializer.data, "token": api_token.token}, - status=status.HTTP_201_CREATED, + {"error": "Workspace is required"}, status=status.HTTP_200_OK ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + api_token = APIToken.objects.create( + label=label, user=request.user, workspace_id=workspace + ) + + serializer = APITokenSerializer(api_token) + # Token will be only vissible while creating + return Response( + {"api_token": serializer.data, "token": api_token.token}, + status=status.HTTP_201_CREATED, + ) + def get(self, request): - try: - api_tokens = APIToken.objects.filter(user=request.user) - serializer = APITokenSerializer(api_tokens, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + api_tokens = APIToken.objects.filter(user=request.user) + serializer = APITokenSerializer(api_tokens, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + def delete(self, request, pk): - try: - api_token = APIToken.objects.get(pk=pk) - api_token.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except APIToken.DoesNotExist: - return Response( - {"error": "Token does not exists"}, status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + api_token = APIToken.objects.get(pk=pk) + api_token.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py index d9b6e502d1d..3f5dcceacbb 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/api/views/asset.py @@ -18,108 +18,58 @@ class FileAssetEndpoint(BaseAPIView): """ def get(self, request, workspace_id, asset_key): - try: - asset_key = str(workspace_id) + "/" + asset_key - files = FileAsset.objects.filter(asset=asset_key) - if files.exists(): - serializer = FileAssetSerializer(files, context={"request": request}, many=True) - return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) - else: - return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + asset_key = str(workspace_id) + "/" + asset_key + files = FileAsset.objects.filter(asset=asset_key) + if files.exists(): + serializer = FileAssetSerializer(files, context={"request": request}, many=True) + return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) + else: + return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) def post(self, request, slug): - try: - serializer = FileAssetSerializer(data=request.data) - if serializer.is_valid(): - # Get the workspace - workspace = Workspace.objects.get(slug=slug) - serializer.save(workspace_id=workspace.id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Workspace.DoesNotExist: - return Response({"error": "Workspace does not exist"}, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = FileAssetSerializer(data=request.data) + if serializer.is_valid(): + # Get the workspace + workspace = Workspace.objects.get(slug=slug) + serializer.save(workspace_id=workspace.id) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def delete(self, request, workspace_id, asset_key): - try: - asset_key = str(workspace_id) + "/" + asset_key - file_asset = FileAsset.objects.get(asset=asset_key) - # Delete the file from storage - file_asset.asset.delete(save=False) - # Delete the file object - file_asset.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except FileAsset.DoesNotExist: - return Response( - {"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + asset_key = str(workspace_id) + "/" + asset_key + file_asset = FileAsset.objects.get(asset=asset_key) + # Delete the file from storage + file_asset.asset.delete(save=False) + # Delete the file object + file_asset.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class UserAssetsEndpoint(BaseAPIView): parser_classes = (MultiPartParser, FormParser) def get(self, request, asset_key): - try: files = FileAsset.objects.filter(asset=asset_key, created_by=request.user) if files.exists(): serializer = FileAssetSerializer(files, context={"request": request}) return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) else: return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) def post(self, request): - try: serializer = FileAssetSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + def delete(self, request, asset_key): - try: file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user) # Delete the file from storage file_asset.asset.delete(save=False) # Delete the file object file_asset.delete() return Response(status=status.HTTP_204_NO_CONTENT) - except FileAsset.DoesNotExist: - return Response( - {"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) diff --git a/apiserver/plane/api/views/auth_extended.py b/apiserver/plane/api/views/auth_extended.py index df3f3aaca83..fbffacff8dc 100644 --- a/apiserver/plane/api/views/auth_extended.py +++ b/apiserver/plane/api/views/auth_extended.py @@ -9,7 +9,6 @@ DjangoUnicodeDecodeError, ) from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode -from django.contrib.sites.shortcuts import get_current_site from django.conf import settings ## Third Party Imports @@ -56,11 +55,11 @@ def get(self, request): return Response( {"email": "Successfully activated"}, status=status.HTTP_200_OK ) - except jwt.ExpiredSignatureError as indentifier: + except jwt.ExpiredSignatureError as _indentifier: return Response( {"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST ) - except jwt.exceptions.DecodeError as indentifier: + except jwt.exceptions.DecodeError as _indentifier: return Response( {"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST ) @@ -128,32 +127,25 @@ def post(self, request, uidb64, token): class ChangePasswordEndpoint(BaseAPIView): def post(self, request): - try: - serializer = ChangePasswordSerializer(data=request.data) - - user = User.objects.get(pk=request.user.id) - if serializer.is_valid(): - # Check old password - if not user.object.check_password(serializer.data.get("old_password")): - return Response( - {"old_password": ["Wrong password."]}, - status=status.HTTP_400_BAD_REQUEST, - ) - # set_password also hashes the password that the user will get - self.object.set_password(serializer.data.get("new_password")) - self.object.save() - response = { - "status": "success", - "code": status.HTTP_200_OK, - "message": "Password updated successfully", - } - - return Response(response) + serializer = ChangePasswordSerializer(data=request.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + user = User.objects.get(pk=request.user.id) + if serializer.is_valid(): + # Check old password + if not user.object.check_password(serializer.data.get("old_password")): + return Response( + {"old_password": ["Wrong password."]}, + status=status.HTTP_400_BAD_REQUEST, + ) + # set_password also hashes the password that the user will get + self.object.set_password(serializer.data.get("new_password")) + self.object.save() + response = { + "status": "success", + "code": status.HTTP_200_OK, + "message": "Password updated successfully", + } + + return Response(response) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/api/views/authentication.py b/apiserver/plane/api/views/authentication.py index aa8ff451168..eadfeef61bb 100644 --- a/apiserver/plane/api/views/authentication.py +++ b/apiserver/plane/api/views/authentication.py @@ -40,228 +40,193 @@ class SignUpEndpoint(BaseAPIView): permission_classes = (AllowAny,) def post(self, request): - try: - if not settings.ENABLE_SIGNUP: - return Response( - { - "error": "New account creation is disabled. Please contact your site administrator" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - email = request.data.get("email", False) - password = request.data.get("password", False) - - ## Raise exception if any of the above are missing - if not email or not password: - return Response( - {"error": "Both email and password are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - email = email.strip().lower() - - try: - validate_email(email) - except ValidationError as e: - return Response( - {"error": "Please provide a valid email address."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Check if the user already exists - if User.objects.filter(email=email).exists(): - return Response( - {"error": "User with this email already exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if not settings.ENABLE_SIGNUP: + return Response( + { + "error": "New account creation is disabled. Please contact your site administrator" + }, + status=status.HTTP_400_BAD_REQUEST, + ) - user = User.objects.create(email=email, username=uuid.uuid4().hex) - user.set_password(password) + email = request.data.get("email", False) + password = request.data.get("password", False) - # settings last actives for the user - user.last_active = timezone.now() - user.last_login_time = timezone.now() - user.last_login_ip = request.META.get("REMOTE_ADDR") - user.last_login_uagent = request.META.get("HTTP_USER_AGENT") - user.token_updated_at = timezone.now() - user.save() + ## Raise exception if any of the above are missing + if not email or not password: + return Response( + {"error": "Both email and password are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) - serialized_user = UserSerializer(user).data + email = email.strip().lower() - access_token, refresh_token = get_tokens_for_user(user) + try: + validate_email(email) + except ValidationError as e: + return Response( + {"error": "Please provide a valid email address."}, + status=status.HTTP_400_BAD_REQUEST, + ) - data = { - "access_token": access_token, - "refresh_token": refresh_token, - "user": serialized_user, - } + # Check if the user already exists + if User.objects.filter(email=email).exists(): + return Response( + {"error": "User with this email already exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) - # Send Analytics - if settings.ANALYTICS_BASE_API: - _ = requests.post( - settings.ANALYTICS_BASE_API, - headers={ - "Content-Type": "application/json", - "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + user = User.objects.create(email=email, username=uuid.uuid4().hex) + user.set_password(password) + + # settings last actives for the user + user.last_active = timezone.now() + user.last_login_time = timezone.now() + user.last_login_ip = request.META.get("REMOTE_ADDR") + user.last_login_uagent = request.META.get("HTTP_USER_AGENT") + user.token_updated_at = timezone.now() + user.save() + + access_token, refresh_token = get_tokens_for_user(user) + + data = { + "access_token": access_token, + "refresh_token": refresh_token, + } + + # Send Analytics + if settings.ANALYTICS_BASE_API: + _ = requests.post( + settings.ANALYTICS_BASE_API, + headers={ + "Content-Type": "application/json", + "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + }, + json={ + "event_id": uuid.uuid4().hex, + "event_data": { + "medium": "email", }, - json={ - "event_id": uuid.uuid4().hex, - "event_data": { - "medium": "email", - }, - "user": {"email": email, "id": str(user.id)}, - "device_ctx": { - "ip": request.META.get("REMOTE_ADDR"), - "user_agent": request.META.get("HTTP_USER_AGENT"), - }, - "event_type": "SIGN_UP", + "user": {"email": email, "id": str(user.id)}, + "device_ctx": { + "ip": request.META.get("REMOTE_ADDR"), + "user_agent": request.META.get("HTTP_USER_AGENT"), }, - ) - - return Response(data, status=status.HTTP_200_OK) - - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + "event_type": "SIGN_UP", + }, ) + return Response(data, status=status.HTTP_200_OK) + class SignInEndpoint(BaseAPIView): permission_classes = (AllowAny,) def post(self, request): - try: - email = request.data.get("email", False) - password = request.data.get("password", False) + email = request.data.get("email", False) + password = request.data.get("password", False) - ## Raise exception if any of the above are missing - if not email or not password: - return Response( - {"error": "Both email and password are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + ## Raise exception if any of the above are missing + if not email or not password: + return Response( + {"error": "Both email and password are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) - email = email.strip().lower() + email = email.strip().lower() - try: - validate_email(email) - except ValidationError as e: - return Response( - {"error": "Please provide a valid email address."}, - status=status.HTTP_400_BAD_REQUEST, - ) + try: + validate_email(email) + except ValidationError as e: + return Response( + {"error": "Please provide a valid email address."}, + status=status.HTTP_400_BAD_REQUEST, + ) - user = User.objects.filter(email=email).first() + user = User.objects.filter(email=email).first() - if user is None: - return Response( - { - "error": "Sorry, we could not find a user with the provided credentials. Please try again." - }, - status=status.HTTP_403_FORBIDDEN, - ) + if user is None: + return Response( + { + "error": "Sorry, we could not find a user with the provided credentials. Please try again." + }, + status=status.HTTP_403_FORBIDDEN, + ) - # Sign up Process - if not user.check_password(password): - return Response( - { - "error": "Sorry, we could not find a user with the provided credentials. Please try again." - }, - status=status.HTTP_403_FORBIDDEN, - ) - if not user.is_active: - return Response( - { - "error": "Your account has been deactivated. Please contact your site administrator." - }, - status=status.HTTP_403_FORBIDDEN, - ) + # Sign up Process + if not user.check_password(password): + return Response( + { + "error": "Sorry, we could not find a user with the provided credentials. Please try again." + }, + status=status.HTTP_403_FORBIDDEN, + ) + if not user.is_active: + return Response( + { + "error": "Your account has been deactivated. Please contact your site administrator." + }, + status=status.HTTP_403_FORBIDDEN, + ) - serialized_user = UserSerializer(user).data - - # settings last active for the user - user.last_active = timezone.now() - user.last_login_time = timezone.now() - user.last_login_ip = request.META.get("REMOTE_ADDR") - user.last_login_uagent = request.META.get("HTTP_USER_AGENT") - user.token_updated_at = timezone.now() - user.save() - - access_token, refresh_token = get_tokens_for_user(user) - # Send Analytics - if settings.ANALYTICS_BASE_API: - _ = requests.post( - settings.ANALYTICS_BASE_API, - headers={ - "Content-Type": "application/json", - "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + # settings last active for the user + user.last_active = timezone.now() + user.last_login_time = timezone.now() + user.last_login_ip = request.META.get("REMOTE_ADDR") + user.last_login_uagent = request.META.get("HTTP_USER_AGENT") + user.token_updated_at = timezone.now() + user.save() + + access_token, refresh_token = get_tokens_for_user(user) + # Send Analytics + if settings.ANALYTICS_BASE_API: + _ = requests.post( + settings.ANALYTICS_BASE_API, + headers={ + "Content-Type": "application/json", + "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + }, + json={ + "event_id": uuid.uuid4().hex, + "event_data": { + "medium": "email", }, - json={ - "event_id": uuid.uuid4().hex, - "event_data": { - "medium": "email", - }, - "user": {"email": email, "id": str(user.id)}, - "device_ctx": { - "ip": request.META.get("REMOTE_ADDR"), - "user_agent": request.META.get("HTTP_USER_AGENT"), - }, - "event_type": "SIGN_IN", + "user": {"email": email, "id": str(user.id)}, + "device_ctx": { + "ip": request.META.get("REMOTE_ADDR"), + "user_agent": request.META.get("HTTP_USER_AGENT"), }, - ) - data = { - "access_token": access_token, - "refresh_token": refresh_token, - "user": serialized_user, - } - - return Response(data, status=status.HTTP_200_OK) - - except Exception as e: - capture_exception(e) - return Response( - { - "error": "Something went wrong. Please try again later or contact the support team." + "event_type": "SIGN_IN", }, - status=status.HTTP_400_BAD_REQUEST, ) + data = { + "access_token": access_token, + "refresh_token": refresh_token, + } + + return Response(data, status=status.HTTP_200_OK) class SignOutEndpoint(BaseAPIView): def post(self, request): - try: - refresh_token = request.data.get("refresh_token", False) + refresh_token = request.data.get("refresh_token", False) - if not refresh_token: - capture_message("No refresh token provided") - return Response( - { - "error": "Something went wrong. Please try again later or contact the support team." - }, - status=status.HTTP_400_BAD_REQUEST, - ) + if not refresh_token: + capture_message("No refresh token provided") + return Response( + {"error": "No refresh token provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) - user = User.objects.get(pk=request.user.id) + user = User.objects.get(pk=request.user.id) - user.last_logout_time = timezone.now() - user.last_logout_ip = request.META.get("REMOTE_ADDR") + user.last_logout_time = timezone.now() + user.last_logout_ip = request.META.get("REMOTE_ADDR") - user.save() + user.save() - token = RefreshToken(refresh_token) - token.blacklist() - return Response({"message": "success"}, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - { - "error": "Something went wrong. Please try again later or contact the support team." - }, - status=status.HTTP_400_BAD_REQUEST, - ) + token = RefreshToken(refresh_token) + token.blacklist() + return Response({"message": "success"}, status=status.HTTP_200_OK) class MagicSignInGenerateEndpoint(BaseAPIView): @@ -270,74 +235,62 @@ class MagicSignInGenerateEndpoint(BaseAPIView): ] def post(self, request): - try: - email = request.data.get("email", False) + email = request.data.get("email", False) - if not email: - return Response( - {"error": "Please provide a valid email address"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if not email: + return Response( + {"error": "Please provide a valid email address"}, + status=status.HTTP_400_BAD_REQUEST, + ) - # Clean up - email = email.strip().lower() - validate_email(email) + # Clean up + email = email.strip().lower() + validate_email(email) - ## Generate a random token - token = ( - "".join(random.choices(string.ascii_lowercase + string.digits, k=4)) - + "-" - + "".join(random.choices(string.ascii_lowercase + string.digits, k=4)) - + "-" - + "".join(random.choices(string.ascii_lowercase + string.digits, k=4)) - ) + ## Generate a random token + token = ( + "".join(random.choices(string.ascii_lowercase, k=4)) + + "-" + + "".join(random.choices(string.ascii_lowercase, k=4)) + + "-" + + "".join(random.choices(string.ascii_lowercase, k=4)) + ) - ri = redis_instance() + ri = redis_instance() - key = "magic_" + str(email) + key = "magic_" + str(email) - # Check if the key already exists in python - if ri.exists(key): - data = json.loads(ri.get(key)) + # Check if the key already exists in python + if ri.exists(key): + data = json.loads(ri.get(key)) - current_attempt = data["current_attempt"] + 1 + current_attempt = data["current_attempt"] + 1 - if data["current_attempt"] > 2: - return Response( - {"error": "Max attempts exhausted. Please try again later."}, - status=status.HTTP_400_BAD_REQUEST, - ) + if data["current_attempt"] > 2: + return Response( + {"error": "Max attempts exhausted. Please try again later."}, + status=status.HTTP_400_BAD_REQUEST, + ) - value = { - "current_attempt": current_attempt, - "email": email, - "token": token, - } - expiry = 600 + value = { + "current_attempt": current_attempt, + "email": email, + "token": token, + } + expiry = 600 - ri.set(key, json.dumps(value), ex=expiry) + ri.set(key, json.dumps(value), ex=expiry) - else: - value = {"current_attempt": 0, "email": email, "token": token} - expiry = 600 + else: + value = {"current_attempt": 0, "email": email, "token": token} + expiry = 600 - ri.set(key, json.dumps(value), ex=expiry) + ri.set(key, json.dumps(value), ex=expiry) - current_site = settings.WEB_URL - magic_link.delay(email, key, token, current_site) + current_site = settings.WEB_URL + magic_link.delay(email, key, token, current_site) - return Response({"key": key}, status=status.HTTP_200_OK) - except ValidationError: - return Response( - {"error": "Please provide a valid email address."}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response({"key": key}, status=status.HTTP_200_OK) class MagicSignInEndpoint(BaseAPIView): @@ -346,113 +299,99 @@ class MagicSignInEndpoint(BaseAPIView): ] def post(self, request): - try: - user_token = request.data.get("token", "").strip() - key = request.data.get("key", False).strip().lower() - - if not key or user_token == "": - return Response( - {"error": "User token and key are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + user_token = request.data.get("token", "").strip() + key = request.data.get("key", False).strip().lower() - ri = redis_instance() - - if ri.exists(key): - data = json.loads(ri.get(key)) - - token = data["token"] - email = data["email"] + if not key or user_token == "": + return Response( + {"error": "User token and key are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) - if str(token) == str(user_token): - if User.objects.filter(email=email).exists(): - user = User.objects.get(email=email) - # Send event to Jitsu for tracking - if settings.ANALYTICS_BASE_API: - _ = requests.post( - settings.ANALYTICS_BASE_API, - headers={ - "Content-Type": "application/json", - "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + ri = redis_instance() + + if ri.exists(key): + data = json.loads(ri.get(key)) + + token = data["token"] + email = data["email"] + + if str(token) == str(user_token): + if User.objects.filter(email=email).exists(): + user = User.objects.get(email=email) + # Send event to Jitsu for tracking + if settings.ANALYTICS_BASE_API: + _ = requests.post( + settings.ANALYTICS_BASE_API, + headers={ + "Content-Type": "application/json", + "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + }, + json={ + "event_id": uuid.uuid4().hex, + "event_data": { + "medium": "code", }, - json={ - "event_id": uuid.uuid4().hex, - "event_data": { - "medium": "code", - }, - "user": {"email": email, "id": str(user.id)}, - "device_ctx": { - "ip": request.META.get("REMOTE_ADDR"), - "user_agent": request.META.get( - "HTTP_USER_AGENT" - ), - }, - "event_type": "SIGN_IN", + "user": {"email": email, "id": str(user.id)}, + "device_ctx": { + "ip": request.META.get("REMOTE_ADDR"), + "user_agent": request.META.get("HTTP_USER_AGENT"), }, - ) - else: - user = User.objects.create( - email=email, - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, + "event_type": "SIGN_IN", + }, ) - # Send event to Jitsu for tracking - if settings.ANALYTICS_BASE_API: - _ = requests.post( - settings.ANALYTICS_BASE_API, - headers={ - "Content-Type": "application/json", - "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + else: + user = User.objects.create( + email=email, + username=uuid.uuid4().hex, + password=make_password(uuid.uuid4().hex), + is_password_autoset=True, + ) + # Send event to Jitsu for tracking + if settings.ANALYTICS_BASE_API: + _ = requests.post( + settings.ANALYTICS_BASE_API, + headers={ + "Content-Type": "application/json", + "X-Auth-Token": settings.ANALYTICS_SECRET_KEY, + }, + json={ + "event_id": uuid.uuid4().hex, + "event_data": { + "medium": "code", }, - json={ - "event_id": uuid.uuid4().hex, - "event_data": { - "medium": "code", - }, - "user": {"email": email, "id": str(user.id)}, - "device_ctx": { - "ip": request.META.get("REMOTE_ADDR"), - "user_agent": request.META.get( - "HTTP_USER_AGENT" - ), - }, - "event_type": "SIGN_UP", + "user": {"email": email, "id": str(user.id)}, + "device_ctx": { + "ip": request.META.get("REMOTE_ADDR"), + "user_agent": request.META.get("HTTP_USER_AGENT"), }, - ) - - user.last_active = timezone.now() - user.last_login_time = timezone.now() - user.last_login_ip = request.META.get("REMOTE_ADDR") - user.last_login_uagent = request.META.get("HTTP_USER_AGENT") - user.token_updated_at = timezone.now() - user.save() - serialized_user = UserSerializer(user).data - - access_token, refresh_token = get_tokens_for_user(user) - data = { - "access_token": access_token, - "refresh_token": refresh_token, - "user": serialized_user, - } + "event_type": "SIGN_UP", + }, + ) - return Response(data, status=status.HTTP_200_OK) + user.last_active = timezone.now() + user.last_login_time = timezone.now() + user.last_login_ip = request.META.get("REMOTE_ADDR") + user.last_login_uagent = request.META.get("HTTP_USER_AGENT") + user.token_updated_at = timezone.now() + user.save() + + access_token, refresh_token = get_tokens_for_user(user) + data = { + "access_token": access_token, + "refresh_token": refresh_token, + } - else: - return Response( - {"error": "Your login code was incorrect. Please try again."}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(data, status=status.HTTP_200_OK) else: return Response( - {"error": "The magic code/link has expired please try again"}, + {"error": "Your login code was incorrect. Please try again."}, status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) + else: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "The magic code/link has expired please try again"}, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index 60b0ec0c62c..7ab660e8142 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -5,10 +5,14 @@ from django.urls import resolve from django.conf import settings from django.utils import timezone -# Third part imports +from django.db import IntegrityError +from django.core.exceptions import ObjectDoesNotExist, ValidationError +# Third part imports +from rest_framework import status from rest_framework import status from rest_framework.viewsets import ModelViewSet +from rest_framework.response import Response from rest_framework.exceptions import APIException from rest_framework.views import APIView from rest_framework.filters import SearchFilter @@ -33,8 +37,6 @@ def initial(self, request, *args, **kwargs): timezone.deactivate() - - class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): model = None @@ -58,17 +60,50 @@ def get_queryset(self): except Exception as e: capture_exception(e) raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST) + + def handle_exception(self, exc): + """ + Handle any exception that occurs, by returning an appropriate response, + or re-raising the error. + """ + try: + response = super().handle_exception(exc) + return response + except Exception as e: + if isinstance(e, IntegrityError): + return Response({"error": "The payload is not valid"}, status=status.HTTP_400_BAD_REQUEST) + + if isinstance(e, ValidationError): + return Response({"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST) + + if isinstance(e, ObjectDoesNotExist): + model_name = str(exc).split(" matching query does not exist.")[0] + return Response({"error": f"{model_name} does not exist."}, status=status.HTTP_404_NOT_FOUND) + + if isinstance(e, KeyError): + capture_exception(e) + return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST) + + print(e) if settings.DEBUG else print("Server Error") + capture_exception(e) + return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def dispatch(self, request, *args, **kwargs): - response = super().dispatch(request, *args, **kwargs) + try: + response = super().dispatch(request, *args, **kwargs) - if settings.DEBUG: - from django.db import connection + if settings.DEBUG: + from django.db import connection - print( - f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}" - ) - return response + print( + f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}" + ) + return response + + except Exception as exc: + response = self.handle_exception(exc) + return exc @property def workspace_slug(self): @@ -104,16 +139,49 @@ def filter_queryset(self, queryset): queryset = backend().filter_queryset(self.request, queryset, self) return queryset + + def handle_exception(self, exc): + """ + Handle any exception that occurs, by returning an appropriate response, + or re-raising the error. + """ + try: + response = super().handle_exception(exc) + return response + except Exception as e: + if isinstance(e, IntegrityError): + return Response({"error": "The payload is not valid"}, status=status.HTTP_400_BAD_REQUEST) + + if isinstance(e, ValidationError): + return Response({"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST) + + if isinstance(e, ObjectDoesNotExist): + model_name = str(exc).split(" matching query does not exist.")[0] + return Response({"error": f"{model_name} does not exist."}, status=status.HTTP_404_NOT_FOUND) + + if isinstance(e, KeyError): + return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST) + + print(e) if settings.DEBUG else print("Server Error") + capture_exception(e) + return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def dispatch(self, request, *args, **kwargs): - response = super().dispatch(request, *args, **kwargs) + try: + response = super().dispatch(request, *args, **kwargs) + + if settings.DEBUG: + from django.db import connection - if settings.DEBUG: - from django.db import connection + print( + f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}" + ) + return response - print( - f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}" - ) - return response + except Exception as exc: + response = self.handle_exception(exc) + return exc @property def workspace_slug(self): diff --git a/apiserver/plane/api/views/config.py b/apiserver/plane/api/views/config.py new file mode 100644 index 00000000000..687cb211c4d --- /dev/null +++ b/apiserver/plane/api/views/config.py @@ -0,0 +1,34 @@ +# Python imports +import os + +# Django imports +from django.conf import settings + +# Third party imports +from rest_framework.permissions import AllowAny +from rest_framework import status +from rest_framework.response import Response +from sentry_sdk import capture_exception + +# Module imports +from .base import BaseAPIView + + +class ConfigurationEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request): + data = {} + data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None) + data["github"] = 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) + ) and os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0") == "1" + data["email_password_login"] = ( + os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1" + ) + data["slack"] = os.environ.get("SLACK_CLIENT_ID", None) + 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 a3d89fa8182..e7d247872ed 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -2,9 +2,7 @@ import json # Django imports -from django.db import IntegrityError from django.db.models import ( - OuterRef, Func, F, Q, @@ -62,28 +60,6 @@ def perform_create(self, serializer): project_id=self.kwargs.get("project_id"), owned_by=self.request.user ) - def perform_destroy(self, instance): - cycle_issues = list( - CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list( - "issue", flat=True - ) - ) - issue_activity.delay( - type="cycle.activity.deleted", - requested_data=json.dumps( - { - "cycle_id": str(self.kwargs.get("pk")), - "issues": [str(issue_id) for issue_id in cycle_issues], - } - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - ) - - return super().perform_destroy(instance) - def get_queryset(self): subquery = CycleFavorite.objects.filter( user=self.request.user, @@ -101,48 +77,84 @@ def get_queryset(self): .select_related("workspace") .select_related("owned_by") .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) .annotate( completed_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( cancelled_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), + filter=Q( + issue_cycle__issue__state__group="cancelled", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( started_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( unstarted_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), + filter=Q( + issue_cycle__issue__state__group="unstarted", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( backlog_issues=Count( "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), + filter=Q( + issue_cycle__issue__state__group="backlog", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) .annotate( completed_estimates=Sum( "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .annotate( started_estimates=Sum( "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), ) ) .prefetch_related( @@ -162,179 +174,189 @@ def get_queryset(self): ) def list(self, request, slug, project_id): - try: - queryset = self.get_queryset() - cycle_view = request.GET.get("cycle_view", "all") - order_by = request.GET.get("order_by", "sort_order") + 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) - - # All Cycles - if cycle_view == "all": - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) + queryset = queryset.order_by(order_by) - # Current Cycle - if cycle_view == "current": - queryset = queryset.filter( - start_date__lte=timezone.now(), - end_date__gte=timezone.now(), - ) + # Current Cycle + if cycle_view == "current": + queryset = queryset.filter( + start_date__lte=timezone.now(), + end_date__gte=timezone.now(), + ) - data = CycleSerializer(queryset, many=True).data + data = CycleSerializer(queryset, many=True).data - if len(data): - assignee_distribution = ( - Issue.objects.filter( - issue_cycle__cycle_id=data[0]["id"], - workspace__slug=slug, - project_id=project_id, - ) - .annotate(first_name=F("assignees__first_name")) - .annotate(last_name=F("assignees__last_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .values("first_name", "last_name", "assignee_id", "avatar") - .annotate(total_issues=Count("assignee_id")) - .annotate( - completed_issues=Count( - "assignee_id", - filter=Q(completed_at__isnull=False), - ) + if len(data): + assignee_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=data[0]["id"], + workspace__slug=slug, + project_id=project_id, + ) + .annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .values("display_name", "assignee_id", "avatar") + .annotate( + total_issues=Count( + "assignee_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) + .annotate( + completed_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) - .annotate( - pending_issues=Count( - "assignee_id", - filter=Q(completed_at__isnull=True), - ) + ) + .annotate( + pending_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) - .order_by("first_name", "last_name") ) + .order_by("display_name") + ) - label_distribution = ( - Issue.objects.filter( - issue_cycle__cycle_id=data[0]["id"], - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate(total_issues=Count("label_id")) - .annotate( - completed_issues=Count( - "label_id", - filter=Q(completed_at__isnull=False), - ) + label_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=data[0]["id"], + workspace__slug=slug, + project_id=project_id, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_issues=Count( + "label_id", + filter=Q(archived_at__isnull=True, is_draft=False), ) - .annotate( - pending_issues=Count( - "label_id", - filter=Q(completed_at__isnull=True), - ) + ) + .annotate( + completed_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) - .order_by("label_name") ) - data[0]["distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } - if data[0]["start_date"] and data[0]["end_date"]: - data[0]["distribution"]["completion_chart"] = burndown_plot( - queryset=queryset.first(), - slug=slug, - project_id=project_id, - cycle_id=data[0]["id"], + .annotate( + pending_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) - - return Response(data, status=status.HTTP_200_OK) - - # Upcoming Cycles - if cycle_view == "upcoming": - queryset = queryset.filter(start_date__gt=timezone.now()) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + .order_by("label_name") ) + data[0]["distribution"] = { + "assignees": assignee_distribution, + "labels": label_distribution, + "completion_chart": {}, + } + if data[0]["start_date"] and data[0]["end_date"]: + data[0]["distribution"]["completion_chart"] = burndown_plot( + queryset=queryset.first(), + slug=slug, + project_id=project_id, + cycle_id=data[0]["id"], + ) - # Completed Cycles - if cycle_view == "completed": - queryset = queryset.filter(end_date__lt=timezone.now()) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) + return Response(data, status=status.HTTP_200_OK) - # Draft Cycles - if cycle_view == "draft": - queryset = queryset.filter( - end_date=None, - start_date=None, - ) + # Upcoming Cycles + if cycle_view == "upcoming": + queryset = queryset.filter(start_date__gt=timezone.now()) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) + # Completed Cycles + if cycle_view == "completed": + queryset = queryset.filter(end_date__lt=timezone.now()) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) - # Incomplete Cycles - if cycle_view == "incomplete": - queryset = queryset.filter( - Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True), - ) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) + # Draft Cycles + if cycle_view == "draft": + queryset = queryset.filter( + end_date=None, + start_date=None, + ) return Response( - {"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK ) - except Exception as e: - capture_exception(e) + # Incomplete Cycles + if cycle_view == "incomplete": + queryset = queryset.filter( + Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True), + ) return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK ) + # If no matching view is found return all cycles + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + def create(self, request, slug, project_id): - try: - if ( - request.data.get("start_date", None) is None - and request.data.get("end_date", None) is None - ) or ( - request.data.get("start_date", None) is not None - and request.data.get("end_date", None) is not None - ): - serializer = CycleSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - owned_by=request.user, - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - else: - return Response( - { - "error": "Both start date and end date are either required or are to be null" - }, - status=status.HTTP_400_BAD_REQUEST, + if ( + request.data.get("start_date", None) is None + and request.data.get("end_date", None) is None + ) or ( + request.data.get("start_date", None) is not None + and request.data.get("end_date", None) is not None + ): + serializer = CycleSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + owned_by=request.user, ) - except Exception as e: - capture_exception(e) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + else: return Response( - {"error": "Something went wrong please try again later"}, + { + "error": "Both start date and end date are either required or are to be null" + }, status=status.HTTP_400_BAD_REQUEST, ) def partial_update(self, request, slug, project_id, pk): - try: - cycle = Cycle.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) + cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + + request_data = request.data - if cycle.end_date is not None and cycle.end_date < timezone.now().date(): + if cycle.end_date is not None and cycle.end_date < timezone.now().date(): + if "sort_order" in request_data: + # Can only change sort order + request_data = { + "sort_order": request_data.get("sort_order", cycle.sort_order) + } + else: return Response( { "error": "The Cycle has already been completed so it cannot be edited" @@ -342,108 +364,138 @@ def partial_update(self, request, slug, project_id, pk): status=status.HTTP_400_BAD_REQUEST, ) - serializer = CycleWriteSerializer(cycle, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Cycle.DoesNotExist: - return Response( - {"error": "Cycle does not exist"}, status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = CycleWriteSerializer(cycle, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk): - try: - queryset = self.get_queryset().get(pk=pk) - - # Assignee Distribution - assignee_distribution = ( - Issue.objects.filter( - issue_cycle__cycle_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(first_name=F("assignees__first_name")) - .annotate(last_name=F("assignees__last_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .annotate(display_name=F("assignees__display_name")) - .values("first_name", "last_name", "assignee_id", "avatar", "display_name") - .annotate(total_issues=Count("assignee_id")) - .annotate( - completed_issues=Count( - "assignee_id", - filter=Q(completed_at__isnull=False), - ) + queryset = self.get_queryset().get(pk=pk) + + # Assignee Distribution + assignee_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=pk, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(first_name=F("assignees__first_name")) + .annotate(last_name=F("assignees__last_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .annotate(display_name=F("assignees__display_name")) + .values("first_name", "last_name", "assignee_id", "avatar", "display_name") + .annotate( + total_issues=Count( + "assignee_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) + .annotate( + completed_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) - .annotate( - pending_issues=Count( - "assignee_id", - filter=Q(completed_at__isnull=True), - ) + ) + .annotate( + pending_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) - .order_by("first_name", "last_name") ) + .order_by("first_name", "last_name") + ) - # Label Distribution - label_distribution = ( - Issue.objects.filter( - issue_cycle__cycle_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate(total_issues=Count("label_id")) - .annotate( - completed_issues=Count( - "label_id", - filter=Q(completed_at__isnull=False), - ) + # Label Distribution + label_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=pk, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_issues=Count( + "label_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) + .annotate( + completed_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) - .annotate( - pending_issues=Count( - "label_id", - filter=Q(completed_at__isnull=True), - ) + ) + .annotate( + pending_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) - .order_by("label_name") ) + .order_by("label_name") + ) - data = CycleSerializer(queryset).data - data["distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } + data = CycleSerializer(queryset).data + data["distribution"] = { + "assignees": assignee_distribution, + "labels": label_distribution, + "completion_chart": {}, + } - if queryset.start_date and queryset.end_date: - data["distribution"]["completion_chart"] = burndown_plot( - queryset=queryset, slug=slug, project_id=project_id, cycle_id=pk - ) - - return Response( - data, - status=status.HTTP_200_OK, - ) - except Cycle.DoesNotExist: - return Response( - {"error": "Cycle Does not exists"}, status=status.HTTP_400_BAD_REQUEST + if queryset.start_date and queryset.end_date: + data["distribution"]["completion_chart"] = burndown_plot( + queryset=queryset, slug=slug, project_id=project_id, cycle_id=pk ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + + return Response( + data, + status=status.HTTP_200_OK, + ) + + def destroy(self, request, slug, project_id, pk): + cycle_issues = list( + CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list( + "issue", flat=True ) + ) + 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), + "issues": [str(issue_id) for issue_id in cycle_issues], + } + ), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + return Response(status=status.HTTP_204_NO_CONTENT) class CycleIssueViewSet(BaseViewSet): @@ -465,22 +517,6 @@ def perform_create(self, serializer): cycle_id=self.kwargs.get("cycle_id"), ) - def perform_destroy(self, instance): - issue_activity.delay( - type="cycle.activity.deleted", - requested_data=json.dumps( - { - "cycle_id": str(self.kwargs.get("cycle_id")), - "issues": [str(instance.issue_id)], - } - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - ) - return super().perform_destroy(instance) - def get_queryset(self): return self.filter_queryset( super() @@ -505,165 +541,174 @@ def get_queryset(self): @method_decorator(gzip_page) def list(self, request, slug, project_id, cycle_id): - try: - order_by = request.GET.get("order_by", "created_at") - group_by = request.GET.get("group_by", False) - filters = issue_filters(request.query_params, "GET") - issues = ( - Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) - .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate(bridge_id=F("issue_cycle__id")) - .filter(project_id=project_id) - .filter(workspace__slug=slug) - .select_related("project") - .select_related("workspace") - .select_related("state") - .select_related("parent") - .prefetch_related("assignees") - .prefetch_related("labels") - .order_by(order_by) - .filter(**filters) - .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") - ) + order_by = request.GET.get("order_by", "created_at") + group_by = request.GET.get("group_by", False) + sub_group_by = request.GET.get("sub_group_by", False) + filters = issue_filters(request.query_params, "GET") + issues = ( + Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) + .annotate(bridge_id=F("issue_cycle__id")) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .order_by(order_by) + .filter(**filters) + .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") + ) + ) - issues_data = IssueStateSerializer(issues, many=True).data - - if group_by: - return Response( - group_results(issues_data, group_by), - status=status.HTTP_200_OK, - ) + issues_data = IssueStateSerializer(issues, many=True).data + if sub_group_by and sub_group_by == group_by: return Response( - issues_data, - status=status.HTTP_200_OK, + {"error": "Group by and sub group by cannot be same"}, + status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) + + if group_by: + grouped_results = group_results(issues_data, group_by, sub_group_by) return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + grouped_results, + status=status.HTTP_200_OK, ) + return Response( + issues_data, status=status.HTTP_200_OK + ) + def create(self, request, slug, project_id, cycle_id): - try: - issues = request.data.get("issues", []) + issues = request.data.get("issues", []) - if not len(issues): - return Response( - {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST - ) + if not len(issues): + return Response( + {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST + ) + + cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=cycle_id + ) - cycle = Cycle.objects.get( - workspace__slug=slug, project_id=project_id, pk=cycle_id + if cycle.end_date is not None and cycle.end_date < timezone.now().date(): + return Response( + { + "error": "The Cycle has already been completed so no new issues can be added" + }, + status=status.HTTP_400_BAD_REQUEST, ) - if cycle.end_date is not None and cycle.end_date < timezone.now().date(): - return Response( - { - "error": "The Cycle has already been completed so no new issues can be added" - }, - status=status.HTTP_400_BAD_REQUEST, + # Get all CycleIssues already created + cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues)) + update_cycle_issue_activity = [] + record_to_create = [] + records_to_update = [] + + for issue in issues: + cycle_issue = [ + cycle_issue + for cycle_issue in cycle_issues + if str(cycle_issue.issue_id) in issues + ] + # Update only when cycle changes + if len(cycle_issue): + if cycle_issue[0].cycle_id != cycle_id: + update_cycle_issue_activity.append( + { + "old_cycle_id": str(cycle_issue[0].cycle_id), + "new_cycle_id": str(cycle_id), + "issue_id": str(cycle_issue[0].issue_id), + } + ) + cycle_issue[0].cycle_id = cycle_id + records_to_update.append(cycle_issue[0]) + else: + record_to_create.append( + CycleIssue( + project_id=project_id, + workspace=cycle.workspace, + created_by=request.user, + updated_by=request.user, + cycle=cycle, + issue_id=issue, + ) ) - # Get all CycleIssues already created - cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues)) - update_cycle_issue_activity = [] - record_to_create = [] - records_to_update = [] - - for issue in issues: - cycle_issue = [ - cycle_issue - for cycle_issue in cycle_issues - if str(cycle_issue.issue_id) in issues - ] - # Update only when cycle changes - if len(cycle_issue): - if cycle_issue[0].cycle_id != cycle_id: - update_cycle_issue_activity.append( - { - "old_cycle_id": str(cycle_issue[0].cycle_id), - "new_cycle_id": str(cycle_id), - "issue_id": str(cycle_issue[0].issue_id), - } - ) - cycle_issue[0].cycle_id = cycle_id - records_to_update.append(cycle_issue[0]) - else: - record_to_create.append( - CycleIssue( - project_id=project_id, - workspace=cycle.workspace, - created_by=request.user, - updated_by=request.user, - cycle=cycle, - issue_id=issue, - ) - ) + CycleIssue.objects.bulk_create( + record_to_create, + batch_size=10, + ignore_conflicts=True, + ) + CycleIssue.objects.bulk_update( + records_to_update, + ["cycle"], + batch_size=10, + ) - CycleIssue.objects.bulk_create( - record_to_create, - batch_size=10, - ignore_conflicts=True, - ) - CycleIssue.objects.bulk_update( - records_to_update, - ["cycle"], - batch_size=10, - ) - - # Capture Issue Activity - issue_activity.delay( - type="cycle.activity.created", - requested_data=json.dumps({"cycles_list": issues}), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "updated_cycle_issues": update_cycle_issue_activity, - "created_cycle_issues": serializers.serialize( - "json", record_to_create - ), - } - ), - ) + # Capture Issue Activity + issue_activity.delay( + type="cycle.activity.created", + requested_data=json.dumps({"cycles_list": issues}), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "updated_cycle_issues": update_cycle_issue_activity, + "created_cycle_issues": serializers.serialize( + "json", record_to_create + ), + } + ), + epoch=int(timezone.now().timestamp()), + ) - # Return all Cycle Issues - return Response( - CycleIssueSerializer(self.get_queryset(), many=True).data, - status=status.HTTP_200_OK, - ) + # Return all Cycle Issues + return Response( + CycleIssueSerializer(self.get_queryset(), many=True).data, + status=status.HTTP_200_OK, + ) - except Cycle.DoesNotExist: - return Response( - {"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + def destroy(self, request, slug, project_id, cycle_id, pk): + cycle_issue = CycleIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id + ) + issue_id = cycle_issue.issue_id + cycle_issue.delete() + issue_activity.delay( + type="cycle.activity.deleted", + requested_data=json.dumps( + { + "cycle_id": str(self.kwargs.get("cycle_id")), + "issues": [str(issue_id)], + } + ), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + return Response(status=status.HTTP_204_NO_CONTENT) class CycleDateCheckEndpoint(BaseAPIView): @@ -672,45 +717,37 @@ class CycleDateCheckEndpoint(BaseAPIView): ] def post(self, request, slug, project_id): - try: - start_date = request.data.get("start_date", False) - end_date = request.data.get("end_date", False) - cycle_id = request.data.get("cycle_id") - if not start_date or not end_date: - return Response( - {"error": "Start date and end date both are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + start_date = request.data.get("start_date", False) + end_date = request.data.get("end_date", False) + cycle_id = request.data.get("cycle_id") + if not start_date or not end_date: + return Response( + {"error": "Start date and end date both are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) - cycles = Cycle.objects.filter( - Q(workspace__slug=slug) - & Q(project_id=project_id) - & ( - Q(start_date__lte=start_date, end_date__gte=start_date) - | Q(start_date__lte=end_date, end_date__gte=end_date) - | Q(start_date__gte=start_date, end_date__lte=end_date) - ) - ).exclude(pk=cycle_id) + cycles = Cycle.objects.filter( + Q(workspace__slug=slug) + & Q(project_id=project_id) + & ( + Q(start_date__lte=start_date, end_date__gte=start_date) + | Q(start_date__lte=end_date, end_date__gte=end_date) + | Q(start_date__gte=start_date, end_date__lte=end_date) + ) + ).exclude(pk=cycle_id) - if cycles.exists(): - return Response( - { - "error": "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", - "status": False, - } - ) - else: - return Response({"status": True}, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) + if cycles.exists(): return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + { + "error": "You have a cycle already on the given dates, if you want to create a draft cycle you can do that by removing dates", + "status": False, + } ) + else: + return Response({"status": True}, status=status.HTTP_200_OK) class CycleFavoriteViewSet(BaseViewSet): - serializer_class = CycleFavoriteSerializer model = CycleFavorite @@ -724,52 +761,21 @@ def get_queryset(self): ) def create(self, request, slug, project_id): - try: - serializer = CycleFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user, project_id=project_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "The cycle is already added to favorites"}, - status=status.HTTP_410_GONE, - ) - else: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = CycleFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user, project_id=project_id) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, slug, project_id, cycle_id): - try: - cycle_favorite = CycleFavorite.objects.get( - project=project_id, - user=request.user, - workspace__slug=slug, - cycle_id=cycle_id, - ) - cycle_favorite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except CycleFavorite.DoesNotExist: - return Response( - {"error": "Cycle is not in favorites"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + cycle_favorite = CycleFavorite.objects.get( + project=project_id, + user=request.user, + workspace__slug=slug, + cycle_id=cycle_id, + ) + cycle_favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class TransferCycleIssueEndpoint(BaseAPIView): @@ -778,55 +784,43 @@ class TransferCycleIssueEndpoint(BaseAPIView): ] def post(self, request, slug, project_id, cycle_id): - try: - new_cycle_id = request.data.get("new_cycle_id", False) - - if not new_cycle_id: - return Response( - {"error": "New Cycle Id is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + new_cycle_id = request.data.get("new_cycle_id", False) - new_cycle = Cycle.objects.get( - workspace__slug=slug, project_id=project_id, pk=new_cycle_id + if not new_cycle_id: + return Response( + {"error": "New Cycle Id is required"}, + status=status.HTTP_400_BAD_REQUEST, ) - if ( - new_cycle.end_date is not None - and new_cycle.end_date < timezone.now().date() - ): - return Response( - { - "error": "The cycle where the issues are transferred is already completed" - }, - status=status.HTTP_400_BAD_REQUEST, - ) + new_cycle = Cycle.objects.get( + workspace__slug=slug, project_id=project_id, pk=new_cycle_id + ) - cycle_issues = CycleIssue.objects.filter( - cycle_id=cycle_id, - project_id=project_id, - workspace__slug=slug, - issue__state__group__in=["backlog", "unstarted", "started"], + if ( + new_cycle.end_date is not None + and new_cycle.end_date < timezone.now().date() + ): + return Response( + { + "error": "The cycle where the issues are transferred is already completed" + }, + status=status.HTTP_400_BAD_REQUEST, ) - updated_cycles = [] - for cycle_issue in cycle_issues: - cycle_issue.cycle_id = new_cycle_id - updated_cycles.append(cycle_issue) + cycle_issues = CycleIssue.objects.filter( + cycle_id=cycle_id, + project_id=project_id, + workspace__slug=slug, + issue__state__group__in=["backlog", "unstarted", "started"], + ) - cycle_issues = CycleIssue.objects.bulk_update( - updated_cycles, ["cycle_id"], batch_size=100 - ) + updated_cycles = [] + for cycle_issue in cycle_issues: + cycle_issue.cycle_id = new_cycle_id + updated_cycles.append(cycle_issue) - return Response({"message": "Success"}, status=status.HTTP_200_OK) - except Cycle.DoesNotExist: - return Response( - {"error": "New Cycle Does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + cycle_issues = CycleIssue.objects.bulk_update( + updated_cycles, ["cycle_id"], batch_size=100 + ) + + return Response({"message": "Success"}, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/estimate.py b/apiserver/plane/api/views/estimate.py index 68de54d7aec..3c2cca4d5af 100644 --- a/apiserver/plane/api/views/estimate.py +++ b/apiserver/plane/api/views/estimate.py @@ -1,6 +1,3 @@ -# Django imports -from django.db import IntegrityError - # Third party imports from rest_framework.response import Response from rest_framework import status @@ -23,7 +20,6 @@ class ProjectEstimatePointEndpoint(BaseAPIView): ] def get(self, request, slug, project_id): - try: project = Project.objects.get(workspace__slug=slug, pk=project_id) if project.estimate_id is not None: estimate_points = EstimatePoint.objects.filter( @@ -34,12 +30,6 @@ def get(self, request, slug, project_id): serializer = EstimatePointSerializer(estimate_points, many=True) return Response(serializer.data, status=status.HTTP_200_OK) return Response([], status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) class BulkEstimatePointEndpoint(BaseViewSet): @@ -50,204 +40,139 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer_class = EstimateSerializer def list(self, request, slug, project_id): - try: - estimates = Estimate.objects.filter( - workspace__slug=slug, project_id=project_id - ).prefetch_related("points").select_related("workspace", "project") - serializer = EstimateReadSerializer(estimates, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + estimates = Estimate.objects.filter( + workspace__slug=slug, project_id=project_id + ).prefetch_related("points").select_related("workspace", "project") + serializer = EstimateReadSerializer(estimates, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, slug, project_id): - try: - if not request.data.get("estimate", False): - return Response( - {"error": "Estimate is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - estimate_points = request.data.get("estimate_points", []) - - if not len(estimate_points) or len(estimate_points) > 8: - return Response( - {"error": "Estimate points are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - estimate_serializer = EstimateSerializer(data=request.data.get("estimate")) - if not estimate_serializer.is_valid(): - return Response( - estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - try: - estimate = estimate_serializer.save(project_id=project_id) - except IntegrityError: - return Response( - {"errror": "Estimate with the name already exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - estimate_points = EstimatePoint.objects.bulk_create( - [ - EstimatePoint( - estimate=estimate, - key=estimate_point.get("key", 0), - value=estimate_point.get("value", ""), - description=estimate_point.get("description", ""), - project_id=project_id, - workspace_id=estimate.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for estimate_point in estimate_points - ], - batch_size=10, - ignore_conflicts=True, + if not request.data.get("estimate", False): + return Response( + {"error": "Estimate is required"}, + status=status.HTTP_400_BAD_REQUEST, ) - estimate_point_serializer = EstimatePointSerializer( - estimate_points, many=True - ) + estimate_points = request.data.get("estimate_points", []) + if not len(estimate_points) or len(estimate_points) > 8: return Response( - { - "estimate": estimate_serializer.data, - "estimate_points": estimate_point_serializer.data, - }, - status=status.HTTP_200_OK, - ) - except Estimate.DoesNotExist: - return Response( - {"error": "Estimate does not exist"}, + {"error": "Estimate points are required"}, status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) + + estimate_serializer = EstimateSerializer(data=request.data.get("estimate")) + if not estimate_serializer.is_valid(): return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + estimate = estimate_serializer.save(project_id=project_id) + estimate_points = EstimatePoint.objects.bulk_create( + [ + EstimatePoint( + estimate=estimate, + key=estimate_point.get("key", 0), + value=estimate_point.get("value", ""), + description=estimate_point.get("description", ""), + project_id=project_id, + workspace_id=estimate.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for estimate_point in estimate_points + ], + batch_size=10, + ignore_conflicts=True, + ) + + estimate_point_serializer = EstimatePointSerializer( + estimate_points, many=True + ) + + return Response( + { + "estimate": estimate_serializer.data, + "estimate_points": estimate_point_serializer.data, + }, + status=status.HTTP_200_OK, + ) def retrieve(self, request, slug, project_id, estimate_id): - try: - estimate = Estimate.objects.get( - pk=estimate_id, workspace__slug=slug, project_id=project_id - ) - serializer = EstimateReadSerializer(estimate) - return Response( - serializer.data, - status=status.HTTP_200_OK, - ) - except Estimate.DoesNotExist: + estimate = Estimate.objects.get( + pk=estimate_id, workspace__slug=slug, project_id=project_id + ) + serializer = EstimateReadSerializer(estimate) + return Response( + serializer.data, + status=status.HTTP_200_OK, + ) + + def partial_update(self, request, slug, project_id, estimate_id): + if not request.data.get("estimate", False): return Response( - {"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST + {"error": "Estimate is required"}, + status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) + + if not len(request.data.get("estimate_points", [])): return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Estimate points are required"}, status=status.HTTP_400_BAD_REQUEST, ) - def partial_update(self, request, slug, project_id, estimate_id): - try: - if not request.data.get("estimate", False): - return Response( - {"error": "Estimate is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not len(request.data.get("estimate_points", [])): - return Response( - {"error": "Estimate points are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - estimate = Estimate.objects.get(pk=estimate_id) + estimate = Estimate.objects.get(pk=estimate_id) - estimate_serializer = EstimateSerializer( - estimate, data=request.data.get("estimate"), partial=True - ) - if not estimate_serializer.is_valid(): - return Response( - estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - try: - estimate = estimate_serializer.save() - except IntegrityError: - return Response( - {"errror": "Estimate with the name already exists"}, - status=status.HTTP_400_BAD_REQUEST, + estimate_serializer = EstimateSerializer( + estimate, data=request.data.get("estimate"), partial=True + ) + if not estimate_serializer.is_valid(): + return Response( + estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + estimate = estimate_serializer.save() + + estimate_points_data = request.data.get("estimate_points", []) + + estimate_points = EstimatePoint.objects.filter( + pk__in=[ + estimate_point.get("id") for estimate_point in estimate_points_data + ], + workspace__slug=slug, + project_id=project_id, + estimate_id=estimate_id, + ) + + updated_estimate_points = [] + for estimate_point in estimate_points: + # Find the data for that estimate point + estimate_point_data = [ + point + for point in estimate_points_data + if point.get("id") == str(estimate_point.id) + ] + if len(estimate_point_data): + estimate_point.value = estimate_point_data[0].get( + "value", estimate_point.value ) + updated_estimate_points.append(estimate_point) - estimate_points_data = request.data.get("estimate_points", []) + EstimatePoint.objects.bulk_update( + updated_estimate_points, ["value"], batch_size=10, + ) - estimate_points = EstimatePoint.objects.filter( - pk__in=[ - estimate_point.get("id") for estimate_point in estimate_points_data - ], - workspace__slug=slug, - project_id=project_id, - estimate_id=estimate_id, - ) - - updated_estimate_points = [] - for estimate_point in estimate_points: - # Find the data for that estimate point - estimate_point_data = [ - point - for point in estimate_points_data - if point.get("id") == str(estimate_point.id) - ] - if len(estimate_point_data): - estimate_point.value = estimate_point_data[0].get( - "value", estimate_point.value - ) - updated_estimate_points.append(estimate_point) - - try: - EstimatePoint.objects.bulk_update( - updated_estimate_points, ["value"], batch_size=10, - ) - except IntegrityError as e: - return Response( - {"error": "Values need to be unique for each key"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True) - return Response( - { - "estimate": estimate_serializer.data, - "estimate_points": estimate_point_serializer.data, - }, - status=status.HTTP_200_OK, - ) - except Estimate.DoesNotExist: - return Response( - {"error": "Estimate does not exist"}, status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True) + return Response( + { + "estimate": estimate_serializer.data, + "estimate_points": estimate_point_serializer.data, + }, + status=status.HTTP_200_OK, + ) def destroy(self, request, slug, project_id, estimate_id): - try: - estimate = Estimate.objects.get( - pk=estimate_id, workspace__slug=slug, project_id=project_id - ) - estimate.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + estimate = Estimate.objects.get( + pk=estimate_id, workspace__slug=slug, project_id=project_id + ) + estimate.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/exporter.py b/apiserver/plane/api/views/exporter.py index 7e14aa82f5a..03da8932f06 100644 --- a/apiserver/plane/api/views/exporter.py +++ b/apiserver/plane/api/views/exporter.py @@ -20,81 +20,62 @@ class ExportIssuesEndpoint(BaseAPIView): serializer_class = ExporterHistorySerializer def post(self, request, slug): - try: - # Get the workspace - workspace = Workspace.objects.get(slug=slug) - - provider = request.data.get("provider", False) - multiple = request.data.get("multiple", False) - project_ids = request.data.get("project", []) - - if provider in ["csv", "xlsx", "json"]: - if not project_ids: - project_ids = Project.objects.filter( - workspace__slug=slug - ).values_list("id", flat=True) - project_ids = [str(project_id) for project_id in project_ids] + # Get the workspace + workspace = Workspace.objects.get(slug=slug) + + provider = request.data.get("provider", False) + multiple = request.data.get("multiple", False) + project_ids = request.data.get("project", []) + + if provider in ["csv", "xlsx", "json"]: + if not project_ids: + project_ids = Project.objects.filter( + workspace__slug=slug + ).values_list("id", flat=True) + project_ids = [str(project_id) for project_id in project_ids] - exporter = ExporterHistory.objects.create( - workspace=workspace, - project=project_ids, - initiated_by=request.user, - provider=provider, - ) + exporter = ExporterHistory.objects.create( + workspace=workspace, + project=project_ids, + initiated_by=request.user, + provider=provider, + ) - issue_export_task.delay( - provider=exporter.provider, - workspace_id=workspace.id, - project_ids=project_ids, - token_id=exporter.token, - multiple=multiple, - slug=slug, - ) - return Response( - { - "message": f"Once the export is ready you will be able to download it" - }, - status=status.HTTP_200_OK, - ) - else: - return Response( - {"error": f"Provider '{provider}' not found."}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Workspace.DoesNotExist: + issue_export_task.delay( + provider=exporter.provider, + workspace_id=workspace.id, + project_ids=project_ids, + token_id=exporter.token, + multiple=multiple, + slug=slug, + ) return Response( - {"error": "Workspace does not exists"}, - status=status.HTTP_400_BAD_REQUEST, + { + "message": f"Once the export is ready you will be able to download it" + }, + status=status.HTTP_200_OK, ) - except Exception as e: - capture_exception(e) + else: return Response( - {"error": "Something went wrong please try again later"}, + {"error": f"Provider '{provider}' not found."}, status=status.HTTP_400_BAD_REQUEST, ) def get(self, request, slug): - try: - exporter_history = ExporterHistory.objects.filter( - workspace__slug=slug - ).select_related("workspace","initiated_by") + exporter_history = ExporterHistory.objects.filter( + workspace__slug=slug + ).select_related("workspace","initiated_by") - if request.GET.get("per_page", False) and request.GET.get("cursor", False): - return self.paginate( - request=request, - queryset=exporter_history, - on_results=lambda exporter_history: ExporterHistorySerializer( - exporter_history, many=True - ).data, - ) - else: - return Response( - {"error": "per_page and cursor are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) + if request.GET.get("per_page", False) and request.GET.get("cursor", False): + return self.paginate( + request=request, + queryset=exporter_history, + on_results=lambda exporter_history: ExporterHistorySerializer( + exporter_history, many=True + ).data, + ) + else: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "per_page and cursor are required"}, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/api/views/external.py b/apiserver/plane/api/views/external.py new file mode 100644 index 00000000000..755879dc699 --- /dev/null +++ b/apiserver/plane/api/views/external.py @@ -0,0 +1,92 @@ +# Python imports +import requests + +# Third party imports +import openai +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import AllowAny +from sentry_sdk import capture_exception + +# Django imports +from django.conf import settings + +# Module imports +from .base import BaseAPIView +from plane.api.permissions import ProjectEntityPermission +from plane.db.models import Workspace, Project +from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer +from plane.utils.integrations.github import get_release_notes + + +class GPTIntegrationEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + def post(self, request, slug, project_id): + if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE: + return Response( + {"error": "OpenAI API key and engine is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + prompt = request.data.get("prompt", False) + task = request.data.get("task", False) + + if not task: + return Response( + {"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST + ) + + final_text = task + "\n" + prompt + + openai.api_key = settings.OPENAI_API_KEY + response = openai.ChatCompletion.create( + model=settings.GPT_ENGINE, + messages=[{"role": "user", "content": final_text}], + temperature=0.7, + max_tokens=1024, + ) + + workspace = Workspace.objects.get(slug=slug) + project = Project.objects.get(pk=project_id) + + text = response.choices[0].message.content.strip() + text_html = text.replace("\n", "
") + return Response( + { + "response": text, + "response_html": text_html, + "project_detail": ProjectLiteSerializer(project).data, + "workspace_detail": WorkspaceLiteSerializer(workspace).data, + }, + status=status.HTTP_200_OK, + ) + + +class ReleaseNotesEndpoint(BaseAPIView): + def get(self, request): + release_notes = get_release_notes() + return Response(release_notes, status=status.HTTP_200_OK) + + +class UnsplashEndpoint(BaseAPIView): + + def get(self, request): + query = request.GET.get("query", False) + page = request.GET.get("page", 1) + per_page = request.GET.get("per_page", 20) + + url = ( + f"https://api.unsplash.com/search/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}" + if query + else f"https://api.unsplash.com/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}" + ) + + headers = { + "Content-Type": "application/json", + } + + resp = requests.get(url=url, headers=headers) + return Response(resp.json(), status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/gpt.py b/apiserver/plane/api/views/gpt.py deleted file mode 100644 index f8065f6d04b..00000000000 --- a/apiserver/plane/api/views/gpt.py +++ /dev/null @@ -1,75 +0,0 @@ -# Python imports -import requests - -# Third party imports -from rest_framework.response import Response -from rest_framework import status -import openai -from sentry_sdk import capture_exception - -# Django imports -from django.conf import settings - -# Module imports -from .base import BaseAPIView -from plane.api.permissions import ProjectEntityPermission -from plane.db.models import Workspace, Project -from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer - - -class GPTIntegrationEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def post(self, request, slug, project_id): - try: - if not settings.OPENAI_API_KEY or not settings.GPT_ENGINE: - return Response( - {"error": "OpenAI API key and engine is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - prompt = request.data.get("prompt", False) - task = request.data.get("task", False) - - if not task: - return Response( - {"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST - ) - - final_text = task + "\n" + prompt - - openai.api_key = settings.OPENAI_API_KEY - response = openai.Completion.create( - model=settings.GPT_ENGINE, - prompt=final_text, - temperature=0.7, - max_tokens=1024, - ) - - workspace = Workspace.objects.get(slug=slug) - project = Project.objects.get(pk=project_id) - - text = response.choices[0].text.strip() - text_html = text.replace("\n", "
") - return Response( - { - "response": text, - "response_html": text_html, - "project_detail": ProjectLiteSerializer(project).data, - "workspace_detail": WorkspaceLiteSerializer(workspace).data, - }, - status=status.HTTP_200_OK, - ) - except (Workspace.DoesNotExist, Project.DoesNotExist) as e: - return Response( - {"error": "Workspace or Project Does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) diff --git a/apiserver/plane/api/views/importer.py b/apiserver/plane/api/views/importer.py index 0a92b3850fe..4060b2bd5a8 100644 --- a/apiserver/plane/api/views/importer.py +++ b/apiserver/plane/api/views/importer.py @@ -39,564 +39,488 @@ from plane.utils.importers.jira import jira_project_issue_summary from plane.bgtasks.importer_task import service_importer from plane.utils.html_processor import strip_tags +from plane.api.permissions import WorkSpaceAdminPermission class ServiceIssueImportSummaryEndpoint(BaseAPIView): def get(self, request, slug, service): - try: - if service == "github": - owner = request.GET.get("owner", False) - repo = request.GET.get("repo", False) + if service == "github": + owner = request.GET.get("owner", False) + repo = request.GET.get("repo", False) - if not owner or not repo: - return Response( - {"error": "Owner and repo are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace_integration = WorkspaceIntegration.objects.get( - integration__provider="github", workspace__slug=slug + if not owner or not repo: + return Response( + {"error": "Owner and repo are required"}, + status=status.HTTP_400_BAD_REQUEST, ) - access_tokens_url = workspace_integration.metadata.get( - "access_tokens_url", False - ) + workspace_integration = WorkspaceIntegration.objects.get( + integration__provider="github", workspace__slug=slug + ) - if not access_tokens_url: - return Response( - { - "error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app." - }, - status=status.HTTP_400_BAD_REQUEST, - ) + access_tokens_url = workspace_integration.metadata.get( + "access_tokens_url", False + ) - issue_count, labels, collaborators = get_github_repo_details( - access_tokens_url, owner, repo - ) + if not access_tokens_url: return Response( { - "issue_count": issue_count, - "labels": labels, - "collaborators": collaborators, + "error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app." }, - status=status.HTTP_200_OK, + status=status.HTTP_400_BAD_REQUEST, ) - if service == "jira": - # Check for all the keys - params = { - "project_key": "Project key is required", - "api_token": "API token is required", - "email": "Email is required", - "cloud_hostname": "Cloud hostname is required", - } - - for key, error_message in params.items(): - if not request.GET.get(key, False): - return Response( - {"error": error_message}, status=status.HTTP_400_BAD_REQUEST - ) - - project_key = request.GET.get("project_key", "") - api_token = request.GET.get("api_token", "") - email = request.GET.get("email", "") - cloud_hostname = request.GET.get("cloud_hostname", "") - - response = jira_project_issue_summary( - email, api_token, project_key, cloud_hostname - ) - if "error" in response: - return Response(response, status=status.HTTP_400_BAD_REQUEST) - else: - return Response( - response, - status=status.HTTP_200_OK, - ) - return Response( - {"error": "Service not supported yet"}, - status=status.HTTP_400_BAD_REQUEST, + issue_count, labels, collaborators = get_github_repo_details( + access_tokens_url, owner, repo ) - except WorkspaceIntegration.DoesNotExist: return Response( - {"error": "Requested integration was not installed in the workspace"}, - status=status.HTTP_400_BAD_REQUEST, + { + "issue_count": issue_count, + "labels": labels, + "collaborators": collaborators, + }, + status=status.HTTP_200_OK, ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + + if service == "jira": + # Check for all the keys + params = { + "project_key": "Project key is required", + "api_token": "API token is required", + "email": "Email is required", + "cloud_hostname": "Cloud hostname is required", + } + + for key, error_message in params.items(): + if not request.GET.get(key, False): + return Response( + {"error": error_message}, status=status.HTTP_400_BAD_REQUEST + ) + + project_key = request.GET.get("project_key", "") + api_token = request.GET.get("api_token", "") + email = request.GET.get("email", "") + cloud_hostname = request.GET.get("cloud_hostname", "") + + response = jira_project_issue_summary( + email, api_token, project_key, cloud_hostname ) + if "error" in response: + return Response(response, status=status.HTTP_400_BAD_REQUEST) + else: + return Response( + response, + status=status.HTTP_200_OK, + ) + return Response( + {"error": "Service not supported yet"}, + status=status.HTTP_400_BAD_REQUEST, + ) class ImportServiceEndpoint(BaseAPIView): + permission_classes = [ + WorkSpaceAdminPermission, + ] def post(self, request, slug, service): - try: - project_id = request.data.get("project_id", False) + project_id = request.data.get("project_id", False) + + if not project_id: + return Response( + {"error": "Project ID is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) - if not project_id: + if service == "github": + data = request.data.get("data", False) + metadata = request.data.get("metadata", False) + config = request.data.get("config", False) + if not data or not metadata or not config: return Response( - {"error": "Project ID is required"}, + {"error": "Data, config and metadata are required"}, status=status.HTTP_400_BAD_REQUEST, ) - workspace = Workspace.objects.get(slug=slug) - - if service == "github": - data = request.data.get("data", False) - metadata = request.data.get("metadata", False) - config = request.data.get("config", False) - if not data or not metadata or not config: - return Response( - {"error": "Data, config and metadata are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - api_token = APIToken.objects.filter( - user=request.user, workspace=workspace - ).first() - if api_token is None: - api_token = APIToken.objects.create( - user=request.user, - label="Importer", - workspace=workspace, - ) - - importer = Importer.objects.create( - service=service, - project_id=project_id, - status="queued", - initiated_by=request.user, - data=data, - metadata=metadata, - token=api_token, - config=config, - created_by=request.user, - updated_by=request.user, + api_token = APIToken.objects.filter( + user=request.user, workspace=workspace + ).first() + if api_token is None: + api_token = APIToken.objects.create( + user=request.user, + label="Importer", + workspace=workspace, ) - service_importer.delay(service, importer.id) - serializer = ImporterSerializer(importer) - return Response(serializer.data, status=status.HTTP_201_CREATED) + importer = Importer.objects.create( + service=service, + project_id=project_id, + status="queued", + initiated_by=request.user, + data=data, + metadata=metadata, + token=api_token, + config=config, + created_by=request.user, + updated_by=request.user, + ) - if service == "jira": - data = request.data.get("data", False) - metadata = request.data.get("metadata", False) - config = request.data.get("config", False) - if not data or not metadata: - return Response( - {"error": "Data, config and metadata are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - api_token = APIToken.objects.filter( - user=request.user, workspace=workspace - ).first() - if api_token is None: - api_token = APIToken.objects.create( - user=request.user, - label="Importer", - workspace=workspace, - ) + service_importer.delay(service, importer.id) + serializer = ImporterSerializer(importer) + return Response(serializer.data, status=status.HTTP_201_CREATED) - importer = Importer.objects.create( - service=service, - project_id=project_id, - status="queued", - initiated_by=request.user, - data=data, - metadata=metadata, - token=api_token, - config=config, - created_by=request.user, - updated_by=request.user, + if service == "jira": + data = request.data.get("data", False) + metadata = request.data.get("metadata", False) + config = request.data.get("config", False) + if not data or not metadata: + return Response( + {"error": "Data, config and metadata are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + api_token = APIToken.objects.filter( + user=request.user, workspace=workspace + ).first() + if api_token is None: + api_token = APIToken.objects.create( + user=request.user, + label="Importer", + workspace=workspace, ) - service_importer.delay(service, importer.id) - serializer = ImporterSerializer(importer) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response( - {"error": "Servivce not supported yet"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except ( - Workspace.DoesNotExist, - WorkspaceIntegration.DoesNotExist, - Project.DoesNotExist, - ) as e: - return Response( - {"error": "Workspace Integration or Project does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + importer = Importer.objects.create( + service=service, + project_id=project_id, + status="queued", + initiated_by=request.user, + data=data, + metadata=metadata, + token=api_token, + config=config, + created_by=request.user, + updated_by=request.user, ) + service_importer.delay(service, importer.id) + serializer = ImporterSerializer(importer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response( + {"error": "Servivce not supported yet"}, + status=status.HTTP_400_BAD_REQUEST, + ) + def get(self, request, slug): - try: - imports = ( - Importer.objects.filter(workspace__slug=slug) - .order_by("-created_at") - .select_related("initiated_by", "project", "workspace") - ) - serializer = ImporterSerializer(imports, many=True) - return Response(serializer.data) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + imports = ( + Importer.objects.filter(workspace__slug=slug) + .order_by("-created_at") + .select_related("initiated_by", "project", "workspace") + ) + serializer = ImporterSerializer(imports, many=True) + return Response(serializer.data) def delete(self, request, slug, service, pk): - try: - importer = Importer.objects.get( - pk=pk, service=service, workspace__slug=slug - ) + importer = Importer.objects.get( + pk=pk, service=service, workspace__slug=slug + ) - if importer.imported_data is not None: - # Delete all imported Issues - imported_issues = importer.imported_data.get("issues", []) - Issue.issue_objects.filter(id__in=imported_issues).delete() - - # Delete all imported Labels - imported_labels = importer.imported_data.get("labels", []) - Label.objects.filter(id__in=imported_labels).delete() - - if importer.service == "jira": - imported_modules = importer.imported_data.get("modules", []) - Module.objects.filter(id__in=imported_modules).delete() - importer.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if importer.imported_data is not None: + # Delete all imported Issues + imported_issues = importer.imported_data.get("issues", []) + Issue.issue_objects.filter(id__in=imported_issues).delete() + + # Delete all imported Labels + imported_labels = importer.imported_data.get("labels", []) + Label.objects.filter(id__in=imported_labels).delete() + + if importer.service == "jira": + imported_modules = importer.imported_data.get("modules", []) + Module.objects.filter(id__in=imported_modules).delete() + importer.delete() + return Response(status=status.HTTP_204_NO_CONTENT) def patch(self, request, slug, service, pk): - try: - importer = Importer.objects.get( - pk=pk, service=service, workspace__slug=slug - ) - serializer = ImporterSerializer(importer, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Importer.DoesNotExist: - return Response( - {"error": "Importer Does not exists"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + importer = Importer.objects.get( + pk=pk, service=service, workspace__slug=slug + ) + serializer = ImporterSerializer(importer, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class UpdateServiceImportStatusEndpoint(BaseAPIView): def post(self, request, slug, project_id, service, importer_id): - try: - importer = Importer.objects.get( - pk=importer_id, - workspace__slug=slug, - project_id=project_id, - service=service, - ) - importer.status = request.data.get("status", "processing") - importer.save() - return Response(status.HTTP_200_OK) - except Importer.DoesNotExist: - return Response( - {"error": "Importer does not exist"}, status=status.HTTP_404_NOT_FOUND - ) + importer = Importer.objects.get( + pk=importer_id, + workspace__slug=slug, + project_id=project_id, + service=service, + ) + importer.status = request.data.get("status", "processing") + importer.save() + return Response(status.HTTP_200_OK) class BulkImportIssuesEndpoint(BaseAPIView): def post(self, request, slug, project_id, service): - try: - # Get the project - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - # Get the default state + # Get the project + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + # Get the default state + default_state = State.objects.filter( + ~Q(name="Triage"), project_id=project_id, default=True + ).first() + # if there is no default state assign any random state + if default_state is None: default_state = State.objects.filter( - ~Q(name="Triage"), project_id=project_id, default=True + ~Q(name="Triage"), project_id=project_id ).first() - # if there is no default state assign any random state - if default_state is None: - default_state = State.objects.filter( - ~Q(name="Triage"), project_id=project_id - ).first() - - # Get the maximum sequence_id - last_id = IssueSequence.objects.filter(project_id=project_id).aggregate( - largest=Max("sequence") - )["largest"] - - last_id = 1 if last_id is None else last_id + 1 - - # Get the maximum sort order - largest_sort_order = Issue.objects.filter( - project_id=project_id, state=default_state - ).aggregate(largest=Max("sort_order"))["largest"] - - largest_sort_order = ( - 65535 if largest_sort_order is None else largest_sort_order + 10000 - ) - # Get the issues_data - issues_data = request.data.get("issues_data", []) + # Get the maximum sequence_id + last_id = IssueSequence.objects.filter(project_id=project_id).aggregate( + largest=Max("sequence") + )["largest"] - if not len(issues_data): - return Response( - {"error": "Issue data is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + last_id = 1 if last_id is None else last_id + 1 - # Issues - bulk_issues = [] - for issue_data in issues_data: - bulk_issues.append( - Issue( - project_id=project_id, - workspace_id=project.workspace_id, - state_id=issue_data.get("state") - if issue_data.get("state", False) - else default_state.id, - name=issue_data.get("name", "Issue Created through Bulk"), - description_html=issue_data.get("description_html", "

"), - description_stripped=( - None - if ( - issue_data.get("description_html") == "" - or issue_data.get("description_html") is None - ) - else strip_tags(issue_data.get("description_html")) - ), - sequence_id=last_id, - sort_order=largest_sort_order, - start_date=issue_data.get("start_date", None), - target_date=issue_data.get("target_date", None), - priority=issue_data.get("priority", None), - created_by=request.user, - ) - ) + # Get the maximum sort order + largest_sort_order = Issue.objects.filter( + project_id=project_id, state=default_state + ).aggregate(largest=Max("sort_order"))["largest"] - largest_sort_order = largest_sort_order + 10000 - last_id = last_id + 1 + largest_sort_order = ( + 65535 if largest_sort_order is None else largest_sort_order + 10000 + ) - issues = Issue.objects.bulk_create( - bulk_issues, - batch_size=100, - ignore_conflicts=True, + # Get the issues_data + issues_data = request.data.get("issues_data", []) + + if not len(issues_data): + return Response( + {"error": "Issue data is required"}, + status=status.HTTP_400_BAD_REQUEST, ) - # Sequences - _ = IssueSequence.objects.bulk_create( - [ - IssueSequence( - issue=issue, - sequence=issue.sequence_id, - project_id=project_id, - workspace_id=project.workspace_id, - ) - for issue in issues - ], - batch_size=100, + # Issues + bulk_issues = [] + for issue_data in issues_data: + bulk_issues.append( + Issue( + project_id=project_id, + workspace_id=project.workspace_id, + state_id=issue_data.get("state") + if issue_data.get("state", False) + else default_state.id, + name=issue_data.get("name", "Issue Created through Bulk"), + description_html=issue_data.get("description_html", "

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

"), + actor=request.user, + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + ) + for comment in comments_list + ] + + _ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100) + + # Attach Links + _ = IssueLink.objects.bulk_create( + [ + IssueLink( + issue=issue, + url=issue_data.get("link", {}).get("url", "https://github.com"), + title=issue_data.get("link", {}).get("title", "Original Issue"), + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + ) + for issue, issue_data in zip(issues, issues_data) + ] + ) - _ = IssueLabel.objects.bulk_create( - bulk_issue_labels, batch_size=100, ignore_conflicts=True - ) + return Response( + {"issues": IssueFlatSerializer(issues, many=True).data}, + status=status.HTTP_201_CREATED, + ) - # Attach Assignees - bulk_issue_assignees = [] - for issue, issue_data in zip(issues, issues_data): - assignees_list = issue_data.get("assignees_list", []) - bulk_issue_assignees = bulk_issue_assignees + [ - IssueAssignee( - issue=issue, - assignee_id=assignee_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for assignee_id in assignees_list - ] - _ = IssueAssignee.objects.bulk_create( - bulk_issue_assignees, batch_size=100, ignore_conflicts=True - ) +class BulkImportModulesEndpoint(BaseAPIView): + def post(self, request, slug, project_id, service): + modules_data = request.data.get("modules_data", []) + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + modules = Module.objects.bulk_create( + [ + Module( + name=module.get("name", uuid.uuid4().hex), + description=module.get("description", ""), + start_date=module.get("start_date", None), + target_date=module.get("target_date", None), + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + ) + for module in modules_data + ], + batch_size=100, + ignore_conflicts=True, + ) - # Track the issue activities - IssueActivity.objects.bulk_create( + modules = Module.objects.filter(id__in=[module.id for module in modules]) + + if len(modules) == len(modules_data): + _ = ModuleLink.objects.bulk_create( [ - IssueActivity( - issue=issue, - actor=request.user, + ModuleLink( + module=module, + url=module_data.get("link", {}).get( + "url", "https://plane.so" + ), + title=module_data.get("link", {}).get( + "title", "Original Issue" + ), project_id=project_id, workspace_id=project.workspace_id, - comment=f"imported the issue from {service}", - verb="created", created_by=request.user, ) - for issue in issues + for module, module_data in zip(modules, modules_data) ], batch_size=100, + ignore_conflicts=True, ) - # Create Comments - bulk_issue_comments = [] - for issue, issue_data in zip(issues, issues_data): - comments_list = issue_data.get("comments_list", []) - bulk_issue_comments = bulk_issue_comments + [ - IssueComment( - issue=issue, - comment_html=comment.get("comment_html", "

"), - actor=request.user, + bulk_module_issues = [] + for module, module_data in zip(modules, modules_data): + module_issues_list = module_data.get("module_issues_list", []) + bulk_module_issues = bulk_module_issues + [ + ModuleIssue( + issue_id=issue, + module=module, project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, ) - for comment in comments_list + for issue in module_issues_list ] - _ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100) - - # Attach Links - _ = IssueLink.objects.bulk_create( - [ - IssueLink( - issue=issue, - url=issue_data.get("link", {}).get("url", "https://github.com"), - title=issue_data.get("link", {}).get("title", "Original Issue"), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for issue, issue_data in zip(issues, issues_data) - ] + _ = ModuleIssue.objects.bulk_create( + bulk_module_issues, batch_size=100, ignore_conflicts=True ) + serializer = ModuleSerializer(modules, many=True) return Response( - {"issues": IssueFlatSerializer(issues, many=True).data}, - status=status.HTTP_201_CREATED, + {"modules": serializer.data}, status=status.HTTP_201_CREATED ) - except Project.DoesNotExist: - return Response( - {"error": "Project Does not exist"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - -class BulkImportModulesEndpoint(BaseAPIView): - def post(self, request, slug, project_id, service): - try: - modules_data = request.data.get("modules_data", []) - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - modules = Module.objects.bulk_create( - [ - Module( - name=module.get("name", uuid.uuid4().hex), - description=module.get("description", ""), - start_date=module.get("start_date", None), - target_date=module.get("target_date", None), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for module in modules_data - ], - batch_size=100, - ignore_conflicts=True, - ) - - modules = Module.objects.filter(id__in=[module.id for module in modules]) - - if len(modules) == len(modules_data): - _ = ModuleLink.objects.bulk_create( - [ - ModuleLink( - module=module, - url=module_data.get("link", {}).get( - "url", "https://plane.so" - ), - title=module_data.get("link", {}).get( - "title", "Original Issue" - ), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for module, module_data in zip(modules, modules_data) - ], - batch_size=100, - ignore_conflicts=True, - ) - - bulk_module_issues = [] - for module, module_data in zip(modules, modules_data): - module_issues_list = module_data.get("module_issues_list", []) - bulk_module_issues = bulk_module_issues + [ - ModuleIssue( - issue_id=issue, - module=module, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - ) - for issue in module_issues_list - ] - - _ = ModuleIssue.objects.bulk_create( - bulk_module_issues, batch_size=100, ignore_conflicts=True - ) - - serializer = ModuleSerializer(modules, many=True) - return Response( - {"modules": serializer.data}, status=status.HTTP_201_CREATED - ) - - else: - return Response( - {"message": "Modules created but issues could not be imported"}, - status=status.HTTP_200_OK, - ) - except Project.DoesNotExist: + else: return Response( - {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + {"message": "Modules created but issues could not be imported"}, + status=status.HTTP_200_OK, ) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 4fbea5f870f..517e9b6de08 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -64,24 +64,17 @@ def perform_create(self, serializer): serializer.save(project_id=self.kwargs.get("project_id")) def destroy(self, request, slug, project_id, pk): - try: - inbox = Inbox.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - # Handle default inbox delete - if inbox.is_default: - return Response( - {"error": "You cannot delete the default inbox"}, - status=status.HTTP_400_BAD_REQUEST, - ) - inbox.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except Exception as e: - capture_exception(e) + inbox = Inbox.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + # Handle default inbox delete + if inbox.is_default: return Response( - {"error": "Something went wronf please try again later"}, + {"error": "You cannot delete the default inbox"}, status=status.HTTP_400_BAD_REQUEST, ) + inbox.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class InboxIssueViewSet(BaseViewSet): @@ -110,274 +103,239 @@ def get_queryset(self): ) def list(self, request, slug, project_id, inbox_id): - try: - filters = issue_filters(request.query_params, "GET") - issues = ( - Issue.objects.filter( - issue_inbox__inbox_id=inbox_id, - workspace__slug=slug, - project_id=project_id, - ) - .filter(**filters) - .annotate(bridge_id=F("issue_inbox__id")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels") - .order_by("issue_inbox__snoozed_till", "issue_inbox__status") - .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .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") + filters = issue_filters(request.query_params, "GET") + issues = ( + Issue.objects.filter( + issue_inbox__inbox_id=inbox_id, + workspace__slug=slug, + project_id=project_id, + ) + .filter(**filters) + .annotate(bridge_id=F("issue_inbox__id")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels") + .order_by("issue_inbox__snoozed_till", "issue_inbox__status") + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .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") ) - .prefetch_related( - Prefetch( - "issue_inbox", - queryset=InboxIssue.objects.only( - "status", "duplicate_to", "snoozed_till", "source" - ), - ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .prefetch_related( + Prefetch( + "issue_inbox", + queryset=InboxIssue.objects.only( + "status", "duplicate_to", "snoozed_till", "source" + ), ) ) - issues_data = IssueStateInboxSerializer(issues, many=True).data + ) + issues_data = IssueStateInboxSerializer(issues, many=True).data + return Response( + issues_data, + status=status.HTTP_200_OK, + ) + + + def create(self, request, slug, project_id, inbox_id): + if not request.data.get("issue", {}).get("name", False): return Response( - issues_data, - status=status.HTTP_200_OK, + {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST ) - except Exception as e: - capture_exception(e) + # Check for valid priority + if not request.data.get("issue", {}).get("priority", "none") in [ + "low", + "medium", + "high", + "urgent", + "none", + ]: return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST ) - def create(self, request, slug, project_id, inbox_id): - try: - if not request.data.get("issue", {}).get("name", False): - return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST - ) + # Create or get state + state, _ = State.objects.get_or_create( + name="Triage", + group="backlog", + description="Default state for managing all Inbox Issues", + project_id=project_id, + color="#ff7700", + ) - # Check for valid priority - if not request.data.get("issue", {}).get("priority", None) in [ - "low", - "medium", - "high", - "urgent", - None, - ]: - return Response( - {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST - ) + # create an issue + issue = Issue.objects.create( + name=request.data.get("issue", {}).get("name"), + description=request.data.get("issue", {}).get("description", {}), + description_html=request.data.get("issue", {}).get( + "description_html", "

" + ), + priority=request.data.get("issue", {}).get("priority", "low"), + project_id=project_id, + state=state, + ) - # Create or get state - state, _ = State.objects.get_or_create( - name="Triage", - group="backlog", - description="Default state for managing all Inbox Issues", - project_id=project_id, - color="#ff7700", - ) + # Create an Issue Activity + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()) + ) + # create an inbox issue + InboxIssue.objects.create( + inbox_id=inbox_id, + project_id=project_id, + issue=issue, + source=request.data.get("source", "in-app"), + ) - # create an issue - issue = Issue.objects.create( - name=request.data.get("issue", {}).get("name"), - description=request.data.get("issue", {}).get("description", {}), - description_html=request.data.get("issue", {}).get( - "description_html", "

" - ), - priority=request.data.get("issue", {}).get("priority", "low"), - project_id=project_id, - state=state, - ) + serializer = IssueStateInboxSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) - # Create an Issue Activity - issue_activity.delay( - type="issue.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=None, - ) - # create an inbox issue - InboxIssue.objects.create( - inbox_id=inbox_id, - project_id=project_id, - issue=issue, - source=request.data.get("source", "in-app"), - ) + def partial_update(self, request, slug, project_id, inbox_id, pk): + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + # Get the project member + project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user) + # Only project members admins and created_by users can access this endpoint + if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id): + return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST) - serializer = IssueStateInboxSerializer(issue) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + # Get issue data + issue_data = request.data.pop("issue", False) + + if bool(issue_data): + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id ) + # Only allow guests and viewers to edit name and description + if project_member.role <= 10: + # viewers and guests since only viewers and guests + issue_data = { + "name": issue_data.get("name", issue.name), + "description_html": issue_data.get("description_html", issue.description_html), + "description": issue_data.get("description", issue.description) + } - def partial_update(self, request, slug, project_id, inbox_id, pk): - try: - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + issue_serializer = IssueCreateSerializer( + issue, data=issue_data, partial=True ) - # Get the project member - project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user) - # Only project members admins and created_by users can access this endpoint - if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id): - return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST) - - # Get issue data - issue_data = request.data.pop("issue", False) - - if bool(issue_data): - issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id - ) - # Only allow guests and viewers to edit name and description - if project_member.role <= 10: - # viewers and guests since only viewers and guests - issue_data = { - "name": issue_data.get("name", issue.name), - "description_html": issue_data.get("description_html", issue.description_html), - "description": issue_data.get("description", issue.description) - } - - issue_serializer = IssueCreateSerializer( - issue, data=issue_data, partial=True - ) - if issue_serializer.is_valid(): - current_instance = issue - # Log all the updates - requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) - if issue is not None: - issue_activity.delay( - type="issue.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=json.dumps( - IssueSerializer(current_instance).data, - cls=DjangoJSONEncoder, - ), - ) - issue_serializer.save() - else: - return Response( - issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST + if issue_serializer.is_valid(): + current_instance = issue + # Log all the updates + requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) + if issue is not None: + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(current_instance).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()) ) - - # Only project admins and members can edit inbox issue attributes - if project_member.role > 10: - serializer = InboxIssueSerializer( - inbox_issue, data=request.data, partial=True + issue_serializer.save() + else: + return Response( + issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - if serializer.is_valid(): - serializer.save() - # Update the issue state if the issue is rejected or marked as duplicate - if serializer.data["status"] in [-1, 2]: - issue = Issue.objects.get( - pk=inbox_issue.issue_id, - workspace__slug=slug, - project_id=project_id, - ) + # Only project admins and members can edit inbox issue attributes + if project_member.role > 10: + serializer = InboxIssueSerializer( + inbox_issue, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + # Update the issue state if the issue is rejected or marked as duplicate + if serializer.data["status"] in [-1, 2]: + issue = Issue.objects.get( + pk=inbox_issue.issue_id, + workspace__slug=slug, + project_id=project_id, + ) + state = State.objects.filter( + group="cancelled", workspace__slug=slug, project_id=project_id + ).first() + if state is not None: + issue.state = state + issue.save() + + # Update the issue state if it is accepted + if serializer.data["status"] in [1]: + issue = Issue.objects.get( + pk=inbox_issue.issue_id, + workspace__slug=slug, + project_id=project_id, + ) + + # Update the issue state only if it is in triage state + if issue.state.name == "Triage": + # Move to default state state = State.objects.filter( - group="cancelled", workspace__slug=slug, project_id=project_id + workspace__slug=slug, project_id=project_id, default=True ).first() if state is not None: issue.state = state issue.save() - # Update the issue state if it is accepted - if serializer.data["status"] in [1]: - issue = Issue.objects.get( - pk=inbox_issue.issue_id, - workspace__slug=slug, - project_id=project_id, - ) - - # Update the issue state only if it is in triage state - if issue.state.name == "Triage": - # Move to default state - state = State.objects.filter( - workspace__slug=slug, project_id=project_id, default=True - ).first() - if state is not None: - issue.state = state - issue.save() - - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - else: - return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK) - except InboxIssue.DoesNotExist: - return Response( - {"error": "Inbox Issue does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + else: + return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, inbox_id, pk): - try: - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) - issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id - ) - serializer = IssueStateInboxSerializer(issue) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + ) + serializer = IssueStateInboxSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, inbox_id, pk): - try: - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) - # Get the project member - project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user) - - if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id): - return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST) - - inbox_issue.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except InboxIssue.DoesNotExist: - return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + # Get the project member + project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user) + + if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id): + return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST) + + # Check the issue status + if inbox_issue.status in [-2, -1, 0, 2]: + # Delete the issue also + Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete() + + inbox_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class InboxIssuePublicViewSet(BaseViewSet): @@ -402,244 +360,200 @@ def get_queryset(self): ) .select_related("issue", "workspace", "project") ) - else: - return InboxIssue.objects.none() + return InboxIssue.objects.none() def list(self, request, slug, project_id, inbox_id): - try: - project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) - if project_deploy_board.inbox is None: - return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) - - filters = issue_filters(request.query_params, "GET") - issues = ( - Issue.objects.filter( - issue_inbox__inbox_id=inbox_id, - workspace__slug=slug, - project_id=project_id, - ) - .filter(**filters) - .annotate(bridge_id=F("issue_inbox__id")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels") - .order_by("issue_inbox__snoozed_till", "issue_inbox__status") - .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .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") - ) - .prefetch_related( - Prefetch( - "issue_inbox", - queryset=InboxIssue.objects.only( - "status", "duplicate_to", "snoozed_till", "source" - ), - ) - ) + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) + + filters = issue_filters(request.query_params, "GET") + issues = ( + Issue.objects.filter( + issue_inbox__inbox_id=inbox_id, + workspace__slug=slug, + project_id=project_id, ) - issues_data = IssueStateInboxSerializer(issues, many=True).data - return Response( - issues_data, - status=status.HTTP_200_OK, + .filter(**filters) + .annotate(bridge_id=F("issue_inbox__id")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels") + .order_by("issue_inbox__snoozed_till", "issue_inbox__status") + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) - except ProjectDeployBoard.DoesNotExist: - return Response({"error": "Project Deploy Board does not exist"}, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) - - def create(self, request, slug, project_id, inbox_id): - try: - project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) - if project_deploy_board.inbox is None: - return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) - - if not request.data.get("issue", {}).get("name", False): - return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") ) - - # Check for valid priority - if not request.data.get("issue", {}).get("priority", None) in [ - "low", - "medium", - "high", - "urgent", - None, - ]: - return Response( - {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .prefetch_related( + Prefetch( + "issue_inbox", + queryset=InboxIssue.objects.only( + "status", "duplicate_to", "snoozed_till", "source" + ), ) - - # Create or get state - state, _ = State.objects.get_or_create( - name="Triage", - group="backlog", - description="Default state for managing all Inbox Issues", - project_id=project_id, - color="#ff7700", ) + ) + issues_data = IssueStateInboxSerializer(issues, many=True).data + return Response( + issues_data, + status=status.HTTP_200_OK, + ) - # create an issue - issue = Issue.objects.create( - name=request.data.get("issue", {}).get("name"), - description=request.data.get("issue", {}).get("description", {}), - description_html=request.data.get("issue", {}).get( - "description_html", "

" - ), - priority=request.data.get("issue", {}).get("priority", "low"), - project_id=project_id, - state=state, - ) + def create(self, request, slug, project_id, inbox_id): + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) - # Create an Issue Activity - issue_activity.delay( - type="issue.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=None, - ) - # create an inbox issue - InboxIssue.objects.create( - inbox_id=inbox_id, - project_id=project_id, - issue=issue, - source=request.data.get("source", "in-app"), + if not request.data.get("issue", {}).get("name", False): + return Response( + {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST ) - serializer = IssueStateInboxSerializer(issue) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) + # Check for valid priority + if not request.data.get("issue", {}).get("priority", "none") in [ + "low", + "medium", + "high", + "urgent", + "none", + ]: return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST ) - def partial_update(self, request, slug, project_id, inbox_id, pk): - try: - project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) - if project_deploy_board.inbox is None: - return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) + # Create or get state + state, _ = State.objects.get_or_create( + name="Triage", + group="backlog", + description="Default state for managing all Inbox Issues", + project_id=project_id, + color="#ff7700", + ) - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) - # Get the project member - if str(inbox_issue.created_by_id) != str(request.user.id): - return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST) + # create an issue + issue = Issue.objects.create( + name=request.data.get("issue", {}).get("name"), + description=request.data.get("issue", {}).get("description", {}), + description_html=request.data.get("issue", {}).get( + "description_html", "

" + ), + priority=request.data.get("issue", {}).get("priority", "low"), + project_id=project_id, + state=state, + ) - # Get issue data - issue_data = request.data.pop("issue", False) + # Create an Issue Activity + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()) + ) + # create an inbox issue + InboxIssue.objects.create( + inbox_id=inbox_id, + project_id=project_id, + issue=issue, + source=request.data.get("source", "in-app"), + ) + serializer = IssueStateInboxSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) - issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id - ) - # viewers and guests since only viewers and guests - issue_data = { - "name": issue_data.get("name", issue.name), - "description_html": issue_data.get("description_html", issue.description_html), - "description": issue_data.get("description", issue.description) - } + def partial_update(self, request, slug, project_id, inbox_id, pk): + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) - issue_serializer = IssueCreateSerializer( - issue, data=issue_data, partial=True - ) + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + # Get the project member + if str(inbox_issue.created_by_id) != str(request.user.id): + return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST) - if issue_serializer.is_valid(): - current_instance = issue - # Log all the updates - requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) - if issue is not None: - issue_activity.delay( - type="issue.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=json.dumps( - IssueSerializer(current_instance).data, - cls=DjangoJSONEncoder, - ), - ) - issue_serializer.save() - return Response(issue_serializer.data, status=status.HTTP_200_OK) - return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except InboxIssue.DoesNotExist: - return Response( - {"error": "Inbox Issue does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + # Get issue data + issue_data = request.data.pop("issue", False) + + + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + ) + # viewers and guests since only viewers and guests + issue_data = { + "name": issue_data.get("name", issue.name), + "description_html": issue_data.get("description_html", issue.description_html), + "description": issue_data.get("description", issue.description) + } + + issue_serializer = IssueCreateSerializer( + issue, data=issue_data, partial=True + ) + + if issue_serializer.is_valid(): + current_instance = issue + # Log all the updates + requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) + if issue is not None: + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(current_instance).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()) + ) + issue_serializer.save() + return Response(issue_serializer.data, status=status.HTTP_200_OK) + return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, inbox_id, pk): - try: - project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) - if project_deploy_board.inbox is None: - return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) - - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) - issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id - ) - serializer = IssueStateInboxSerializer(issue) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) + + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + ) + serializer = IssueStateInboxSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, inbox_id, pk): - try: - project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) - if project_deploy_board.inbox is None: - return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) + project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id) + if project_deploy_board.inbox is None: + return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST) - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) - - if str(inbox_issue.created_by_id) != str(request.user.id): - return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST) + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) - inbox_issue.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except InboxIssue.DoesNotExist: - return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if str(inbox_issue.created_by_id) != str(request.user.id): + return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST) + inbox_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 5213baf637e..cc911b53716 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -1,8 +1,7 @@ # Python improts import uuid - +import requests # Django imports -from django.db import IntegrityError from django.contrib.auth.hashers import make_password # Third party imports @@ -26,72 +25,46 @@ delete_github_installation, ) from plane.api.permissions import WorkSpaceAdminPermission - +from plane.utils.integrations.slack import slack_oauth class IntegrationViewSet(BaseViewSet): serializer_class = IntegrationSerializer model = Integration def create(self, request): - try: - serializer = IntegrationSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) + serializer = IntegrationSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, pk): + integration = Integration.objects.get(pk=pk) + if integration.verified: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Verified integrations cannot be updated"}, status=status.HTTP_400_BAD_REQUEST, ) - def partial_update(self, request, pk): - try: - integration = Integration.objects.get(pk=pk) - if integration.verified: - return Response( - {"error": "Verified integrations cannot be updated"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = IntegrationSerializer( - integration, data=request.data, partial=True - ) + serializer = IntegrationSerializer( + integration, data=request.data, partial=True + ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Integration.DoesNotExist: - return Response( - {"error": "Integration Does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except Exception as e: - capture_exception(e) + def destroy(self, request, pk): + integration = Integration.objects.get(pk=pk) + if integration.verified: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Verified integrations cannot be updated"}, status=status.HTTP_400_BAD_REQUEST, ) - def destroy(self, request, pk): - try: - integration = Integration.objects.get(pk=pk) - if integration.verified: - return Response( - {"error": "Verified integrations cannot be updated"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - integration.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except Integration.DoesNotExist: - return Response( - {"error": "Integration Does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) + integration.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class WorkspaceIntegrationViewSet(BaseViewSet): @@ -111,119 +84,88 @@ def get_queryset(self): ) def create(self, request, slug, provider): - try: - workspace = Workspace.objects.get(slug=slug) - integration = Integration.objects.get(provider=provider) - config = {} - if provider == "github": - installation_id = request.data.get("installation_id", None) - if not installation_id: - return Response( - {"error": "Installation ID is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - metadata = get_github_metadata(installation_id) - config = {"installation_id": installation_id} - - if provider == "slack": - metadata = request.data.get("metadata", {}) - 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"}, - status=status.HTTP_400_BAD_REQUEST, - ) - config = {"team_id": team_id, "access_token": access_token} - - # Create a bot user - bot_user = User.objects.create( - email=f"{uuid.uuid4().hex}@plane.so", - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, - is_bot=True, - first_name=integration.title, - avatar=integration.avatar_url - if integration.avatar_url is not None - else "", - ) + workspace = Workspace.objects.get(slug=slug) + integration = Integration.objects.get(provider=provider) + config = {} + if provider == "github": + installation_id = request.data.get("installation_id", None) + if not installation_id: + return Response( + {"error": "Installation ID is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + metadata = get_github_metadata(installation_id) + config = {"installation_id": installation_id} - # Create an API Token for the bot user - api_token = APIToken.objects.create( - user=bot_user, - user_type=1, # bot user - workspace=workspace, - ) + if provider == "slack": + code = request.data.get("code", False) - workspace_integration = WorkspaceIntegration.objects.create( - workspace=workspace, - integration=integration, - actor=bot_user, - api_token=api_token, - metadata=metadata, - config=config, - ) + if not code: + return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST) - # Add bot user as a member of workspace - _ = WorkspaceMember.objects.create( - workspace=workspace_integration.workspace, - member=bot_user, - role=20, - ) - return Response( - WorkspaceIntegrationSerializer(workspace_integration).data, - status=status.HTTP_201_CREATED, - ) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "Integration is already active in the workspace"}, - status=status.HTTP_410_GONE, - ) - else: - capture_exception(e) + 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": "Something went wrong please try again later"}, + {"error": "Slack could not be installed. Please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) - except (Workspace.DoesNotExist, Integration.DoesNotExist) as e: - capture_exception(e) - return Response( - {"error": "Workspace or Integration not found"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + config = {"team_id": team_id, "access_token": access_token} + + # Create a bot user + bot_user = User.objects.create( + email=f"{uuid.uuid4().hex}@plane.so", + username=uuid.uuid4().hex, + password=make_password(uuid.uuid4().hex), + is_password_autoset=True, + is_bot=True, + first_name=integration.title, + avatar=integration.avatar_url + if integration.avatar_url is not None + else "", + ) - def destroy(self, request, slug, pk): - try: - workspace_integration = WorkspaceIntegration.objects.get( - pk=pk, workspace__slug=slug - ) + # Create an API Token for the bot user + api_token = APIToken.objects.create( + user=bot_user, + user_type=1, # bot user + workspace=workspace, + ) - if workspace_integration.integration.provider == "github": - installation_id = workspace_integration.config.get( - "installation_id", False - ) - if installation_id: - delete_github_installation(installation_id=installation_id) + workspace_integration = WorkspaceIntegration.objects.create( + workspace=workspace, + integration=integration, + actor=bot_user, + api_token=api_token, + metadata=metadata, + config=config, + ) - workspace_integration.delete() - return Response(status=status.HTTP_204_NO_CONTENT) + # Add bot user as a member of workspace + _ = WorkspaceMember.objects.create( + workspace=workspace_integration.workspace, + member=bot_user, + role=20, + ) + return Response( + WorkspaceIntegrationSerializer(workspace_integration).data, + status=status.HTTP_201_CREATED, + ) - except WorkspaceIntegration.DoesNotExist: - return Response( - {"error": "Workspace Integration Does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + def destroy(self, request, slug, pk): + workspace_integration = WorkspaceIntegration.objects.get( + pk=pk, workspace__slug=slug + ) + + if workspace_integration.integration.provider == "github": + installation_id = workspace_integration.config.get( + "installation_id", False ) + if installation_id: + delete_github_installation(installation_id=installation_id) + + workspace_integration.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index 4cf07c70595..f2035639e46 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -30,31 +30,25 @@ class GithubRepositoriesEndpoint(BaseAPIView): ] def get(self, request, slug, workspace_integration_id): - try: - page = request.GET.get("page", 1) - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) - - if workspace_integration.integration.provider != "github": - return Response( - {"error": "Not a github integration"}, - status=status.HTTP_400_BAD_REQUEST, - ) + page = request.GET.get("page", 1) + workspace_integration = WorkspaceIntegration.objects.get( + workspace__slug=slug, pk=workspace_integration_id + ) - access_tokens_url = workspace_integration.metadata["access_tokens_url"] - repositories_url = ( - workspace_integration.metadata["repositories_url"] - + f"?per_page=100&page={page}" - ) - repositories = get_github_repos(access_tokens_url, repositories_url) - return Response(repositories, status=status.HTTP_200_OK) - except WorkspaceIntegration.DoesNotExist: + if workspace_integration.integration.provider != "github": return Response( - {"error": "Workspace Integration Does not exists"}, + {"error": "Not a github integration"}, status=status.HTTP_400_BAD_REQUEST, ) + access_tokens_url = workspace_integration.metadata["access_tokens_url"] + repositories_url = ( + workspace_integration.metadata["repositories_url"] + + f"?per_page=100&page={page}" + ) + repositories = get_github_repos(access_tokens_url, repositories_url) + return Response(repositories, status=status.HTTP_200_OK) + class GithubRepositorySyncViewSet(BaseViewSet): permission_classes = [ @@ -76,88 +70,75 @@ def get_queryset(self): ) def create(self, request, slug, project_id, workspace_integration_id): - try: - name = request.data.get("name", False) - url = request.data.get("url", False) - config = request.data.get("config", {}) - repository_id = request.data.get("repository_id", False) - owner = request.data.get("owner", False) - - if not name or not url or not repository_id or not owner: - return Response( - {"error": "Name, url, repository_id and owner are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + name = request.data.get("name", False) + url = request.data.get("url", False) + config = request.data.get("config", {}) + repository_id = request.data.get("repository_id", False) + owner = request.data.get("owner", False) - # Get the workspace integration - workspace_integration = WorkspaceIntegration.objects.get( - pk=workspace_integration_id + if not name or not url or not repository_id or not owner: + return Response( + {"error": "Name, url, repository_id and owner are required"}, + status=status.HTTP_400_BAD_REQUEST, ) - # Delete the old repository object - GithubRepositorySync.objects.filter( - project_id=project_id, workspace__slug=slug - ).delete() - GithubRepository.objects.filter( - project_id=project_id, workspace__slug=slug - ).delete() - - # Create repository - repo = GithubRepository.objects.create( - name=name, - url=url, - config=config, - repository_id=repository_id, - owner=owner, - project_id=project_id, - ) + # Get the workspace integration + workspace_integration = WorkspaceIntegration.objects.get( + pk=workspace_integration_id + ) - # Create a Label for github - label = Label.objects.filter( - name="GitHub", - project_id=project_id, - ).first() + # Delete the old repository object + GithubRepositorySync.objects.filter( + project_id=project_id, workspace__slug=slug + ).delete() + GithubRepository.objects.filter( + project_id=project_id, workspace__slug=slug + ).delete() + + # Create repository + repo = GithubRepository.objects.create( + name=name, + url=url, + config=config, + repository_id=repository_id, + owner=owner, + project_id=project_id, + ) - if label is None: - label = Label.objects.create( - name="GitHub", - project_id=project_id, - description="Label to sync Plane issues with GitHub issues", - color="#003773", - ) + # Create a Label for github + label = Label.objects.filter( + name="GitHub", + project_id=project_id, + ).first() - # Create repo sync - repo_sync = GithubRepositorySync.objects.create( - repository=repo, - workspace_integration=workspace_integration, - actor=workspace_integration.actor, - credentials=request.data.get("credentials", {}), + if label is None: + label = Label.objects.create( + name="GitHub", project_id=project_id, - label=label, + description="Label to sync Plane issues with GitHub issues", + color="#003773", ) - # Add bot as a member in the project - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, role=20, project_id=project_id - ) + # Create repo sync + repo_sync = GithubRepositorySync.objects.create( + repository=repo, + workspace_integration=workspace_integration, + actor=workspace_integration.actor, + credentials=request.data.get("credentials", {}), + project_id=project_id, + label=label, + ) - # Return Response - return Response( - GithubRepositorySyncSerializer(repo_sync).data, - status=status.HTTP_201_CREATED, - ) + # Add bot as a member in the project + _ = ProjectMember.objects.get_or_create( + member=workspace_integration.actor, role=20, project_id=project_id + ) - except WorkspaceIntegration.DoesNotExist: - return Response( - {"error": "Workspace Integration does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + # Return Response + return Response( + GithubRepositorySyncSerializer(repo_sync).data, + status=status.HTTP_201_CREATED, + ) class GithubIssueSyncViewSet(BaseViewSet): @@ -177,42 +158,30 @@ def perform_create(self, serializer): class BulkCreateGithubIssueSyncEndpoint(BaseAPIView): def post(self, request, slug, project_id, repo_sync_id): - try: - project = Project.objects.get(pk=project_id, workspace__slug=slug) - - github_issue_syncs = request.data.get("github_issue_syncs", []) - github_issue_syncs = GithubIssueSync.objects.bulk_create( - [ - GithubIssueSync( - issue_id=github_issue_sync.get("issue"), - repo_issue_id=github_issue_sync.get("repo_issue_id"), - issue_url=github_issue_sync.get("issue_url"), - github_issue_id=github_issue_sync.get("github_issue_id"), - repository_sync_id=repo_sync_id, - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for github_issue_sync in github_issue_syncs - ], - batch_size=100, - ignore_conflicts=True, - ) + project = Project.objects.get(pk=project_id, workspace__slug=slug) + + github_issue_syncs = request.data.get("github_issue_syncs", []) + github_issue_syncs = GithubIssueSync.objects.bulk_create( + [ + GithubIssueSync( + issue_id=github_issue_sync.get("issue"), + repo_issue_id=github_issue_sync.get("repo_issue_id"), + issue_url=github_issue_sync.get("issue_url"), + github_issue_id=github_issue_sync.get("github_issue_id"), + repository_sync_id=repo_sync_id, + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for github_issue_sync in github_issue_syncs + ], + batch_size=100, + ignore_conflicts=True, + ) - serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - except Project.DoesNotExist: - return Response( - {"error": "Project does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = GithubIssueSyncSerializer(github_issue_syncs, many=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) class GithubCommentSyncViewSet(BaseViewSet): diff --git a/apiserver/plane/api/views/integration/slack.py b/apiserver/plane/api/views/integration/slack.py index 498dd0607d0..863b6ba0cf2 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): @@ -33,41 +34,45 @@ def get_queryset(self): def create(self, request, slug, project_id, workspace_integration_id): try: - serializer = SlackProjectSyncSerializer(data=request.data) + code = request.data.get("code", False) - workspace_integration = WorkspaceIntegration.objects.get( - workspace__slug=slug, pk=workspace_integration_id - ) - - if serializer.is_valid(): - serializer.save( - project_id=project_id, - workspace_integration_id=workspace_integration_id, + if not code: + return Response( + {"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST ) - workspace_integration = WorkspaceIntegration.objects.get( - pk=workspace_integration_id, workspace__slug=slug - ) + slack_response = slack_oauth(code=code) - _ = ProjectMember.objects.get_or_create( - member=workspace_integration.actor, role=20, project_id=project_id - ) + workspace_integration = WorkspaceIntegration.objects.get( + workspace__slug=slug, pk=workspace_integration_id + ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except IntegrityError: - return Response( - {"error": "Slack is already enabled for the project"}, - status=status.HTTP_400_BAD_REQUEST, + workspace_integration = WorkspaceIntegration.objects.get( + pk=workspace_integration_id, workspace__slug=slug ) - except WorkspaceIntegration.DoesNotExist: - return Response( - {"error": "Workspace Integration does not exist"}, - status=status.HTTP_400_BAD_REQUEST, + 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, ) - except Exception as e: - print(e) + _ = ProjectMember.objects.get_or_create( + member=workspace_integration.actor, role=20, project_id=project_id + ) + serializer = SlackProjectSyncSerializer(slack_project_sync) + return Response(serializer.data, status=status.HTTP_200_OK) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"error": "Slack is already installed for the project"}, + status=status.HTTP_410_GONE, + ) + capture_exception(e) return Response( - {"error": "Something went wrong please try again later"}, + {"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 802431d2e8b..ff7f526915c 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -4,6 +4,7 @@ from itertools import chain # Django imports +from django.utils import timezone from django.db.models import ( Prefetch, OuterRef, @@ -17,18 +18,18 @@ When, Exists, Max, + IntegerField, ) from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from django.db.models.functions import Coalesce -from django.conf import settings +from django.db import IntegrityError # Third Party imports from rest_framework.response import Response from rest_framework import status from rest_framework.parsers import MultiPartParser, FormParser -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from sentry_sdk import capture_exception # Module imports @@ -38,7 +39,6 @@ IssueActivitySerializer, IssueCommentSerializer, IssuePropertySerializer, - LabelSerializer, IssueSerializer, LabelSerializer, IssueFlatSerializer, @@ -50,10 +50,11 @@ IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssueRelationSerializer, + RelatedIssueSerializer, IssuePublicSerializer, ) from plane.api.permissions import ( - WorkspaceEntityPermission, ProjectEntityPermission, WorkSpaceAdminPermission, ProjectMemberPermission, @@ -75,12 +76,12 @@ CommentReaction, ProjectDeployBoard, IssueVote, + IssueRelation, ProjectPublicMember, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters -from plane.bgtasks.export_task import issue_export_task class IssueViewSet(BaseViewSet): @@ -106,47 +107,6 @@ def get_serializer_class(self): "workspace__id", ] - def perform_create(self, serializer): - serializer.save(project_id=self.kwargs.get("project_id")) - - def perform_update(self, serializer): - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - current_instance = ( - self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() - ) - if current_instance is not None: - issue_activity.delay( - type="issue.activity.updated", - requested_data=requested_data, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueSerializer(current_instance).data, cls=DjangoJSONEncoder - ), - ) - - return super().perform_update(serializer) - - def perform_destroy(self, instance): - current_instance = ( - self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() - ) - if current_instance is not None: - issue_activity.delay( - type="issue.activity.deleted", - requested_data=json.dumps( - {"issue_id": str(self.kwargs.get("pk", None))} - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueSerializer(current_instance).data, cls=DjangoJSONEncoder - ), - ) - return super().perform_destroy(instance) - def get_queryset(self): return ( Issue.issue_objects.annotate( @@ -169,288 +129,318 @@ def get_queryset(self): queryset=IssueReaction.objects.select_related("actor"), ) ) - ) + ).distinct() @method_decorator(gzip_page) def list(self, request, slug, project_id): - try: - filters = issue_filters(request.query_params, "GET") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - self.get_queryset() - .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") - ) + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + self.get_queryset() + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) + ) - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order if order_by_param == "priority" else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issues = IssueLiteSerializer(issue_queryset, many=True).data - - ## Grouping the results - group_by = request.GET.get("group_by", False) - if group_by: - return Response( - group_results(issues, group_by), status=status.HTTP_200_OK + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) - return Response(issues, status=status.HTTP_200_OK) + issues = IssueLiteSerializer(issue_queryset, many=True).data - except Exception as e: - capture_exception(e) + ## Grouping the results + group_by = request.GET.get("group_by", False) + sub_group_by = request.GET.get("sub_group_by", False) + if sub_group_by and sub_group_by == group_by: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Group by and sub group by cannot be same"}, status=status.HTTP_400_BAD_REQUEST, ) - def create(self, request, slug, project_id): - try: - project = Project.objects.get(pk=project_id) - - serializer = IssueCreateSerializer( - data=request.data, - context={ - "project_id": project_id, - "workspace_id": project.workspace_id, - "default_assignee_id": project.default_assignee_id, - }, + if group_by: + grouped_results = group_results(issues, group_by, sub_group_by) + return Response( + grouped_results, + status=status.HTTP_200_OK, ) - if serializer.is_valid(): - serializer.save() + return Response(issues, status=status.HTTP_200_OK) - # Track the issue - issue_activity.delay( - type="issue.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(serializer.data.get("id", None)), - project_id=str(project_id), - current_instance=None, - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def create(self, request, slug, project_id): + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) - except Project.DoesNotExist: - return Response( - {"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND + if serializer.is_valid(): + serializer.save() + + # Track the issue + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk=None): - try: - issue = Issue.issue_objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) - except Issue.DoesNotExist: - return Response( - {"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND + issue = Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ).get(workspace__slug=slug, project_id=project_id, pk=pk) + return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) + + def partial_update(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(issue).data, cls=DjangoJSONEncoder + ) + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + serializer = IssueCreateSerializer(issue, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, pk=None): + issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + current_instance = json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ) + issue.delete() + issue_activity.delay( + type="issue.activity.deleted", + requested_data=json.dumps({"issue_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + ) + return Response(status=status.HTTP_204_NO_CONTENT) class UserWorkSpaceIssues(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug): - try: - filters = issue_filters(request.query_params, "GET") - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - Issue.issue_objects.filter( - (Q(assignees__in=[request.user]) | Q(created_by=request.user)), - workspace__slug=slug, - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .select_related("project") - .select_related("workspace") - .select_related("state") - .select_related("parent") - .prefetch_related("assignees") - .prefetch_related("labels") - .order_by(order_by_param) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) + filters = issue_filters(request.query_params, "GET") + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + Issue.issue_objects.filter( + ( + Q(assignees__in=[request.user]) + | Q(created_by=request.user) + | Q(issue_subscribers__subscriber=request.user) + ), + workspace__slug=slug, + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .order_by(order_by_param) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), ) - .filter(**filters) - ).distinct() - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] + ) + .filter(**filters) + ).distinct() + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order if order_by_param == "priority" else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issues = IssueLiteSerializer(issue_queryset, many=True).data + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) - ## Grouping the results - group_by = request.GET.get("group_by", False) - if group_by: - return Response( - group_results(issues, group_by), status=status.HTTP_200_OK - ) + issues = IssueLiteSerializer(issue_queryset, many=True).data - return Response(issues, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) + ## Grouping the results + group_by = request.GET.get("group_by", False) + sub_group_by = request.GET.get("sub_group_by", False) + if sub_group_by and sub_group_by == group_by: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Group by and sub group by cannot be same"}, status=status.HTTP_400_BAD_REQUEST, ) + if group_by: + grouped_results = group_results(issues, group_by, sub_group_by) + return Response( + grouped_results, + status=status.HTTP_200_OK, + ) + + return Response(issues, status=status.HTTP_200_OK) + class WorkSpaceIssuesEndpoint(BaseAPIView): permission_classes = [ @@ -459,20 +449,13 @@ class WorkSpaceIssuesEndpoint(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug): - try: - issues = ( - Issue.issue_objects.filter(workspace__slug=slug) - .filter(project__project_projectmember__member=self.request.user) - .order_by("-created_at") - ) - serializer = IssueSerializer(issues, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + issues = ( + Issue.issue_objects.filter(workspace__slug=slug) + .filter(project__project_projectmember__member=self.request.user) + .order_by("-created_at") + ) + serializer = IssueSerializer(issues, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) class IssueActivityEndpoint(BaseAPIView): @@ -482,42 +465,35 @@ class IssueActivityEndpoint(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug, project_id, issue_id): - try: - issue_activities = ( - IssueActivity.objects.filter(issue_id=issue_id) - .filter( - ~Q(field="comment"), - project__project_projectmember__member=self.request.user, - ) - .select_related("actor", "workspace", "issue", "project") - ).order_by("created_at") - issue_comments = ( - IssueComment.objects.filter(issue_id=issue_id) - .filter(project__project_projectmember__member=self.request.user) - .order_by("created_at") - .select_related("actor", "issue", "project", "workspace") - .prefetch_related( - Prefetch( - "comment_reactions", - queryset=CommentReaction.objects.select_related("actor"), - ) + issue_activities = ( + IssueActivity.objects.filter(issue_id=issue_id) + .filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + project__project_projectmember__member=self.request.user, + ) + .select_related("actor", "workspace", "issue", "project") + ).order_by("created_at") + issue_comments = ( + IssueComment.objects.filter(issue_id=issue_id) + .filter(project__project_projectmember__member=self.request.user) + .order_by("created_at") + .select_related("actor", "issue", "project", "workspace") + .prefetch_related( + Prefetch( + "comment_reactions", + queryset=CommentReaction.objects.select_related("actor"), ) ) - issue_activities = IssueActivitySerializer(issue_activities, many=True).data - issue_comments = IssueCommentSerializer(issue_comments, many=True).data + ) + issue_activities = IssueActivitySerializer(issue_activities, many=True).data + issue_comments = IssueCommentSerializer(issue_comments, many=True).data - result_list = sorted( - chain(issue_activities, issue_comments), - key=lambda instance: instance["created_at"], - ) + result_list = sorted( + chain(issue_activities, issue_comments), + key=lambda instance: instance["created_at"], + ) - return Response(result_list, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(result_list, status=status.HTTP_200_OK) class IssueCommentViewSet(BaseViewSet): @@ -532,61 +508,6 @@ class IssueCommentViewSet(BaseViewSet): "workspace__id", ] - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - issue_id=self.kwargs.get("issue_id"), - actor=self.request.user if self.request.user is not None else None, - ) - issue_activity.delay( - type="comment.activity.created", - requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id")), - project_id=str(self.kwargs.get("project_id")), - current_instance=None, - ) - - def perform_update(self, serializer): - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - current_instance = ( - self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() - ) - if current_instance is not None: - issue_activity.delay( - type="comment.activity.updated", - requested_data=requested_data, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueCommentSerializer(current_instance).data, - cls=DjangoJSONEncoder, - ), - ) - - return super().perform_update(serializer) - - def perform_destroy(self, instance): - current_instance = ( - self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() - ) - if current_instance is not None: - issue_activity.delay( - type="comment.activity.deleted", - requested_data=json.dumps( - {"comment_id": str(self.kwargs.get("pk", None))} - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueCommentSerializer(current_instance).data, - cls=DjangoJSONEncoder, - ), - ) - return super().perform_destroy(instance) - def get_queryset(self): return self.filter_queryset( super() @@ -610,66 +531,98 @@ def get_queryset(self): .distinct() ) + def create(self, request, slug, project_id, issue_id): + serializer = IssueCommentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + actor=request.user, + ) + issue_activity.delay( + type="comment.activity.created", + requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id")), + project_id=str(self.kwargs.get("project_id")), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -class IssuePropertyViewSet(BaseViewSet): - serializer_class = IssuePropertySerializer - model = IssueProperty - permission_classes = [ - ProjectEntityPermission, - ] - - filterset_fields = [] - - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), user=self.request.user + def partial_update(self, request, slug, project_id, issue_id, pk): + issue_comment = IssueComment.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk ) - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(user=self.request.user) - .filter(project__project_projectmember__member=self.request.user) - .select_related("project") - .select_related("workspace") + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + IssueCommentSerializer(issue_comment).data, + cls=DjangoJSONEncoder, ) + serializer = IssueCommentSerializer( + issue_comment, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="comment.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def list(self, request, slug, project_id): - queryset = self.get_queryset() - serializer = IssuePropertySerializer(queryset, many=True) - return Response( - serializer.data[0] if len(serializer.data) > 0 else [], - status=status.HTTP_200_OK, + def destroy(self, request, slug, project_id, issue_id, pk): + issue_comment = IssueComment.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk + ) + current_instance = json.dumps( + IssueCommentSerializer(issue_comment).data, + cls=DjangoJSONEncoder, + ) + issue_comment.delete() + issue_activity.delay( + type="comment.activity.deleted", + requested_data=json.dumps({"comment_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), ) + return Response(status=status.HTTP_204_NO_CONTENT) - def create(self, request, slug, project_id): - try: - issue_property, created = IssueProperty.objects.get_or_create( - user=request.user, - project_id=project_id, - ) - if not created: - issue_property.properties = request.data.get("properties", {}) - issue_property.save() +class IssueUserDisplayPropertyEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] - serializer = IssuePropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_200_OK) + def post(self, request, slug, project_id): + issue_property, created = IssueProperty.objects.get_or_create( + user=request.user, + project_id=project_id, + ) + if not created: issue_property.properties = request.data.get("properties", {}) issue_property.save() - serializer = IssuePropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_201_CREATED) + issue_property.properties = request.data.get("properties", {}) + issue_property.save() + serializer = IssuePropertySerializer(issue_property) + return Response(serializer.data, status=status.HTTP_201_CREATED) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + def get(self, request, slug, project_id): + issue_property, _ = IssueProperty.objects.get_or_create( + user=request.user, project_id=project_id + ) + serializer = IssuePropertySerializer(issue_property) + return Response(serializer.data, status=status.HTTP_200_OK) class LabelViewSet(BaseViewSet): @@ -679,10 +632,18 @@ class LabelViewSet(BaseViewSet): ProjectMemberPermission, ] - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - ) + def create(self, request, slug, project_id): + try: + serializer = LabelSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except IntegrityError: + return Response( + {"error": "Label with the same name already exists in the project"}, + status=status.HTTP_400_BAD_REQUEST, + ) def get_queryset(self): return self.filter_queryset( @@ -705,33 +666,26 @@ class BulkDeleteIssuesEndpoint(BaseAPIView): ] def delete(self, request, slug, project_id): - try: - issue_ids = request.data.get("issue_ids", []) + issue_ids = request.data.get("issue_ids", []) - if not len(issue_ids): - return Response( - {"error": "Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - issues = Issue.issue_objects.filter( - workspace__slug=slug, project_id=project_id, pk__in=issue_ids + if not len(issue_ids): + return Response( + {"error": "Issue IDs are required"}, + status=status.HTTP_400_BAD_REQUEST, ) - total_issues = len(issues) + issues = Issue.issue_objects.filter( + workspace__slug=slug, project_id=project_id, pk__in=issue_ids + ) - issues.delete() + total_issues = len(issues) - return Response( - {"message": f"{total_issues} issues were deleted"}, - status=status.HTTP_200_OK, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + issues.delete() + + return Response( + {"message": f"{total_issues} issues were deleted"}, + status=status.HTTP_200_OK, + ) class SubIssuesEndpoint(BaseAPIView): @@ -741,110 +695,102 @@ class SubIssuesEndpoint(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug, project_id, issue_id): - try: - sub_issues = ( - Issue.issue_objects.filter(parent_id=issue_id, workspace__slug=slug) - .select_related("project") - .select_related("workspace") - .select_related("state") - .select_related("parent") - .prefetch_related("assignees") - .prefetch_related("labels") - .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .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") - ) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) - ) + sub_issues = ( + Issue.issue_objects.filter(parent_id=issue_id, workspace__slug=slug) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) - - state_distribution = ( - State.objects.filter( - workspace__slug=slug, state_issue__parent_id=issue_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") + ) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), ) - .annotate(state_group=F("group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") ) + ) - result = { - item["state_group"]: item["state_count"] for item in state_distribution - } + state_distribution = ( + State.objects.filter(workspace__slug=slug, state_issue__parent_id=issue_id) + .annotate(state_group=F("group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) - serializer = IssueLiteSerializer( - sub_issues, - many=True, - ) - return Response( - { - "sub_issues": serializer.data, - "state_distribution": result, - }, - status=status.HTTP_200_OK, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + result = { + item["state_group"]: item["state_count"] for item in state_distribution + } + + serializer = IssueLiteSerializer( + sub_issues, + many=True, + ) + return Response( + { + "sub_issues": serializer.data, + "state_distribution": result, + }, + status=status.HTTP_200_OK, + ) # Assign multiple sub issues def post(self, request, slug, project_id, issue_id): - try: - parent_issue = Issue.issue_objects.get(pk=issue_id) - sub_issue_ids = request.data.get("sub_issue_ids", []) + parent_issue = Issue.issue_objects.get(pk=issue_id) + sub_issue_ids = request.data.get("sub_issue_ids", []) - if not len(sub_issue_ids): - return Response( - {"error": "Sub Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if not len(sub_issue_ids): + return Response( + {"error": "Sub Issue IDs are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) - sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) + sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) - for sub_issue in sub_issues: - sub_issue.parent = parent_issue + for sub_issue in sub_issues: + sub_issue.parent = parent_issue - _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) + _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) - updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) + updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) - return Response( - IssueFlatSerializer(updated_sub_issues, many=True).data, - status=status.HTTP_200_OK, - ) - except Issue.DoesNotExist: - return Response( - {"Parent Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + # Track the issue + _ = [ + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"parent": str(issue_id)}), + actor_id=str(request.user.id), + issue_id=str(sub_issue_id), + project_id=str(project_id), + current_instance=json.dumps({"parent": str(sub_issue_id)}), + epoch=int(timezone.now().timestamp()), ) + for sub_issue_id in sub_issue_ids + ] + + return Response( + IssueFlatSerializer(updated_sub_issues, many=True).data, + status=status.HTTP_200_OK, + ) class IssueLinkViewSet(BaseViewSet): @@ -855,60 +801,6 @@ class IssueLinkViewSet(BaseViewSet): model = IssueLink serializer_class = IssueLinkSerializer - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), - issue_id=self.kwargs.get("issue_id"), - ) - issue_activity.delay( - type="link.activity.created", - requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id")), - project_id=str(self.kwargs.get("project_id")), - current_instance=None, - ) - - def perform_update(self, serializer): - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) - current_instance = ( - self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() - ) - if current_instance is not None: - issue_activity.delay( - type="link.activity.updated", - requested_data=requested_data, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueLinkSerializer(current_instance).data, - cls=DjangoJSONEncoder, - ), - ) - - return super().perform_update(serializer) - - def perform_destroy(self, instance): - current_instance = ( - self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() - ) - if current_instance is not None: - issue_activity.delay( - type="link.activity.deleted", - requested_data=json.dumps( - {"link_id": str(self.kwargs.get("pk", None))} - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - IssueLinkSerializer(current_instance).data, - cls=DjangoJSONEncoder, - ), - ) - return super().perform_destroy(instance) - def get_queryset(self): return ( super() @@ -921,44 +813,96 @@ def get_queryset(self): .distinct() ) + def create(self, request, slug, project_id, issue_id): + serializer = IssueLinkSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + ) + issue_activity.delay( + type="link.activity.created", + requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id")), + project_id=str(self.kwargs.get("project_id")), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, issue_id, pk): + issue_link = IssueLink.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk + ) + requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + IssueLinkSerializer(issue_link).data, + cls=DjangoJSONEncoder, + ) + serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="link.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, pk): + issue_link = IssueLink.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk + ) + current_instance = json.dumps( + IssueLinkSerializer(issue_link).data, + cls=DjangoJSONEncoder, + ) + issue_activity.delay( + type="link.activity.deleted", + requested_data=json.dumps({"link_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + ) + issue_link.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + class BulkCreateIssueLabelsEndpoint(BaseAPIView): def post(self, request, slug, project_id): - try: - label_data = request.data.get("label_data", []) - project = Project.objects.get(pk=project_id) - - labels = Label.objects.bulk_create( - [ - Label( - name=label.get("name", "Migrated"), - description=label.get("description", "Migrated Issue"), - color="#" + "%06x" % random.randint(0, 0xFFFFFF), - project_id=project_id, - workspace_id=project.workspace_id, - created_by=request.user, - updated_by=request.user, - ) - for label in label_data - ], - batch_size=50, - ignore_conflicts=True, - ) + label_data = request.data.get("label_data", []) + project = Project.objects.get(pk=project_id) + + labels = Label.objects.bulk_create( + [ + Label( + name=label.get("name", "Migrated"), + description=label.get("description", "Migrated Issue"), + color="#" + "%06x" % random.randint(0, 0xFFFFFF), + project_id=project_id, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for label in label_data + ], + batch_size=50, + ignore_conflicts=True, + ) - return Response( - {"labels": LabelSerializer(labels, many=True).data}, - status=status.HTTP_201_CREATED, - ) - except Project.DoesNotExist: - return Response( - {"error": "Project Does not exist"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response( + {"labels": LabelSerializer(labels, many=True).data}, + status=status.HTTP_201_CREATED, + ) class IssueAttachmentEndpoint(BaseAPIView): @@ -970,64 +914,46 @@ class IssueAttachmentEndpoint(BaseAPIView): parser_classes = (MultiPartParser, FormParser) def post(self, request, slug, project_id, issue_id): - try: - serializer = IssueAttachmentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(project_id=project_id, issue_id=issue_id) - issue_activity.delay( - type="attachment.activity.created", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - serializer.data, - cls=DjangoJSONEncoder, - ), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - def delete(self, request, slug, project_id, issue_id, pk): - try: - issue_attachment = IssueAttachment.objects.get(pk=pk) - issue_attachment.asset.delete(save=False) - issue_attachment.delete() + serializer = IssueAttachmentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id, issue_id=issue_id) issue_activity.delay( - type="attachment.activity.deleted", + type="attachment.activity.created", requested_data=None, actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id", None)), project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, + current_instance=json.dumps( + serializer.data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - return Response(status=status.HTTP_204_NO_CONTENT) - except IssueAttachment.DoesNotExist: - return Response( - {"error": "Issue Attachment does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) + def delete(self, request, slug, project_id, issue_id, pk): + issue_attachment = IssueAttachment.objects.get(pk=pk) + issue_attachment.asset.delete(save=False) + issue_attachment.delete() + issue_activity.delay( + type="attachment.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + + return Response(status=status.HTTP_204_NO_CONTENT) def get(self, request, slug, project_id, issue_id): - try: - issue_attachments = IssueAttachment.objects.filter( - issue_id=issue_id, workspace__slug=slug, project_id=project_id - ) - serilaizer = IssueAttachmentSerializer(issue_attachments, many=True) - return Response(serilaizer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + issue_attachments = IssueAttachment.objects.filter( + issue_id=issue_id, workspace__slug=slug, project_id=project_id + ) + serializer = IssueAttachmentSerializer(issue_attachments, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) class IssueArchiveViewSet(BaseViewSet): @@ -1058,169 +984,136 @@ def get_queryset(self): @method_decorator(gzip_page) def list(self, request, slug, project_id): - try: - filters = issue_filters(request.query_params, "GET") - show_sub_issues = request.GET.get("show_sub_issues", "true") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - self.get_queryset() - .filter(**filters) - .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") - ) + filters = issue_filters(request.query_params, "GET") + show_sub_issues = request.GET.get("show_sub_issues", "true") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + self.get_queryset() + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) + ) - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order if order_by_param == "priority" else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issue_queryset = ( - issue_queryset - if show_sub_issues == "true" - else issue_queryset.filter(parent__isnull=True) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueLiteSerializer(issue_queryset, many=True).data + issue_queryset = ( + issue_queryset + if show_sub_issues == "true" + else issue_queryset.filter(parent__isnull=True) + ) - ## Grouping the results - group_by = request.GET.get("group_by", False) - if group_by: - return Response( - group_results(issues, group_by), status=status.HTTP_200_OK - ) + issues = IssueLiteSerializer(issue_queryset, many=True).data - return Response(issues, status=status.HTTP_200_OK) + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + return Response(group_results(issues, group_by), status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(issues, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk=None): - try: - issue = Issue.objects.get( - workspace__slug=slug, - project_id=project_id, - archived_at__isnull=False, - pk=pk, - ) - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) - except Issue.DoesNotExist: - return Response( - {"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + issue = Issue.objects.get( + workspace__slug=slug, + project_id=project_id, + archived_at__isnull=False, + pk=pk, + ) + return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) def unarchive(self, request, slug, project_id, pk=None): - try: - issue = Issue.objects.get( - workspace__slug=slug, - project_id=project_id, - archived_at__isnull=False, - pk=pk, - ) - issue.archived_at = None - issue.save() - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"archived_at": None}), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=None, - ) - - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) - except Issue.DoesNotExist: - return Response( - {"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong, please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + issue = Issue.objects.get( + workspace__slug=slug, + project_id=project_id, + archived_at__isnull=False, + pk=pk, + ) + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"archived_at": None}), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ), + epoch=int(timezone.now().timestamp()), + ) + issue.archived_at = None + issue.save() + + return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) class IssueSubscriberViewSet(BaseViewSet): @@ -1262,122 +1155,75 @@ def get_queryset(self): ) def list(self, request, slug, project_id, issue_id): - try: - members = ( - ProjectMember.objects.filter( - workspace__slug=slug, project_id=project_id - ) - .annotate( - is_subscribed=Exists( - IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - subscriber=OuterRef("member"), - ) + members = ( + ProjectMember.objects.filter(workspace__slug=slug, project_id=project_id) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + subscriber=OuterRef("member"), ) ) - .select_related("member") - ) - serializer = ProjectMemberLiteSerializer(members, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": e}, - status=status.HTTP_400_BAD_REQUEST, ) + .select_related("member") + ) + serializer = ProjectMemberLiteSerializer(members, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, issue_id, subscriber_id): - try: - issue_subscriber = IssueSubscriber.objects.get( - project=project_id, - subscriber=subscriber_id, - workspace__slug=slug, - issue=issue_id, - ) - issue_subscriber.delete() - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - except IssueSubscriber.DoesNotExist: - return Response( - {"error": "User is not subscribed to this issue"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=subscriber_id, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) def subscribe(self, request, slug, project_id, issue_id): - try: - if IssueSubscriber.objects.filter( - issue_id=issue_id, - subscriber=request.user, - workspace__slug=slug, - project=project_id, - ).exists(): - return Response( - {"message": "User already subscribed to the issue."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - subscriber = IssueSubscriber.objects.create( - issue_id=issue_id, - subscriber_id=request.user.id, - project_id=project_id, - ) - serilaizer = IssueSubscriberSerializer(subscriber) - return Response(serilaizer.data, status=status.HTTP_201_CREATED) - except Exception as e: - capture_exception(e) + if IssueSubscriber.objects.filter( + issue_id=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists(): return Response( - {"error": "Something went wrong, please try again later"}, + {"message": "User already subscribed to the issue."}, status=status.HTTP_400_BAD_REQUEST, ) + subscriber = IssueSubscriber.objects.create( + issue_id=issue_id, + subscriber_id=request.user.id, + project_id=project_id, + ) + serializer = IssueSubscriberSerializer(subscriber) + return Response(serializer.data, status=status.HTTP_201_CREATED) + def unsubscribe(self, request, slug, project_id, issue_id): - try: - issue_subscriber = IssueSubscriber.objects.get( - project=project_id, - subscriber=request.user, - workspace__slug=slug, - issue=issue_id, - ) - issue_subscriber.delete() - return Response( - status=status.HTTP_204_NO_CONTENT, - ) - except IssueSubscriber.DoesNotExist: - return Response( - {"error": "User subscribed to this issue"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + issue_subscriber = IssueSubscriber.objects.get( + project=project_id, + subscriber=request.user, + workspace__slug=slug, + issue=issue_id, + ) + issue_subscriber.delete() + return Response( + status=status.HTTP_204_NO_CONTENT, + ) def subscription_status(self, request, slug, project_id, issue_id): - try: - issue_subscriber = IssueSubscriber.objects.filter( - issue=issue_id, - subscriber=request.user, - workspace__slug=slug, - project=project_id, - ).exists() - return Response({"subscribed": issue_subscriber}, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong, please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + issue_subscriber = IssueSubscriber.objects.filter( + issue=issue_id, + subscriber=request.user, + workspace__slug=slug, + project=project_id, + ).exists() + return Response({"subscribed": issue_subscriber}, status=status.HTTP_200_OK) class IssueReactionViewSet(BaseViewSet): @@ -1399,35 +1245,50 @@ def get_queryset(self): .distinct() ) - def perform_create(self, serializer): - serializer.save( - issue_id=self.kwargs.get("issue_id"), - project_id=self.kwargs.get("project_id"), - actor=self.request.user, - ) - - def destroy(self, request, slug, project_id, issue_id, reaction_code): - try: - issue_reaction = IssueReaction.objects.get( - workspace__slug=slug, - project_id=project_id, + def create(self, request, slug, project_id, issue_id): + serializer = IssueReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( issue_id=issue_id, - reaction=reaction_code, + project_id=project_id, actor=request.user, ) - issue_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except IssueReaction.DoesNotExist: - return Response( - {"error": "Issue reaction does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, reaction_code): + issue_reaction = IssueReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(issue_reaction.id), + } + ), + epoch=int(timezone.now().timestamp()), + ) + issue_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class CommentReactionViewSet(BaseViewSet): @@ -1449,35 +1310,51 @@ def get_queryset(self): .distinct() ) - def perform_create(self, serializer): - serializer.save( - actor=self.request.user, - comment_id=self.kwargs.get("comment_id"), - project_id=self.kwargs.get("project_id"), - ) - - def destroy(self, request, slug, project_id, comment_id, reaction_code): - try: - comment_reaction = CommentReaction.objects.get( - workspace__slug=slug, + def create(self, request, slug, project_id, comment_id): + serializer = CommentReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( project_id=project_id, + actor_id=request.user.id, comment_id=comment_id, - reaction=reaction_code, - actor=request.user, - ) - comment_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except CommentReaction.DoesNotExist: - return Response( - {"error": "Comment reaction does not exist"}, - status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=None, + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, comment_id, reaction_code): + comment_reaction = CommentReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + comment_id=comment_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="comment_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(comment_reaction.id), + "comment_id": str(comment_id), + } + ), + epoch=int(timezone.now().timestamp()), + ) + comment_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class IssueCommentPublicViewSet(BaseViewSet): @@ -1489,137 +1366,109 @@ class IssueCommentPublicViewSet(BaseViewSet): "workspace__id", ] + def get_permissions(self): + if self.action in ["list", "retrieve"]: + self.permission_classes = [ + AllowAny, + ] + else: + self.permission_classes = [ + IsAuthenticated, + ] + + return super(IssueCommentPublicViewSet, self).get_permissions() + def get_queryset(self): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.comments: - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter(access="EXTERNAL") - .select_related("project") - .select_related("workspace") - .select_related("issue") - .annotate( - is_member=Exists( - ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - member_id=self.request.user.id, + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.comments: + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(access="EXTERNAL") + .select_related("project") + .select_related("workspace") + .select_related("issue") + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + member_id=self.request.user.id, + ) ) ) - ) - .distinct() - ) - else: + .distinct() + ).order_by("created_at") + return IssueComment.objects.none() + except ProjectDeployBoard.DoesNotExist: return IssueComment.objects.none() def create(self, request, slug, project_id, issue_id): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.comments: - return Response( - {"error": "Comments are not enabled for this project"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = IssueCommentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - issue_id=issue_id, - actor=request.user, - access="EXTERNAL", - ) - issue_activity.delay( - type="comment.activity.created", - requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=None, - ) - if not ProjectMember.objects.filter( - project_id=project_id, - member=request.user, - ).exists(): - # Add the user for workspace tracking - _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, - member=request.user, - ) + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) + if not project_deploy_board.comments: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Comments are not enabled for this project"}, status=status.HTTP_400_BAD_REQUEST, ) - def partial_update(self, request, slug, project_id, issue_id, pk): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.comments: - return Response( - {"error": "Comments are not enabled for this project"}, - status=status.HTTP_400_BAD_REQUEST, - ) - comment = IssueComment.objects.get( - workspace__slug=slug, pk=pk, actor=request.user + serializer = IssueCommentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + actor=request.user, + access="EXTERNAL", ) - serializer = IssueCommentSerializer( - comment, data=request.data, partial=True + issue_activity.delay( + type="comment.activity.created", + requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), ) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="comment.activity.updated", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=json.dumps( - IssueCommentSerializer(comment).data, - cls=DjangoJSONEncoder, - ), + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist): - return Response( - {"error": "IssueComent Does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - def destroy(self, request, slug, project_id, issue_id, pk): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - if not project_deploy_board.comments: - return Response( - {"error": "Comments are not enabled for this project"}, - status=status.HTTP_400_BAD_REQUEST, - ) - comment = IssueComment.objects.get( - workspace__slug=slug, pk=pk, project_id=project_id, actor=request.user + def partial_update(self, request, slug, project_id, issue_id, pk): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.comments: + return Response( + {"error": "Comments are not enabled for this project"}, + status=status.HTTP_400_BAD_REQUEST, ) + comment = IssueComment.objects.get( + workspace__slug=slug, pk=pk, actor=request.user + ) + serializer = IssueCommentSerializer(comment, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() issue_activity.delay( - type="comment.activity.deleted", - requested_data=json.dumps({"comment_id": str(pk)}), + type="comment.activity.updated", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), actor_id=str(request.user.id), issue_id=str(issue_id), project_id=str(project_id), @@ -1627,20 +1476,38 @@ def destroy(self, request, slug, project_id, issue_id, pk): IssueCommentSerializer(comment).data, cls=DjangoJSONEncoder, ), + epoch=int(timezone.now().timestamp()), ) - comment.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist): - return Response( - {"error": "IssueComent Does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, pk): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.comments: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Comments are not enabled for this project"}, status=status.HTTP_400_BAD_REQUEST, ) + comment = IssueComment.objects.get( + workspace__slug=slug, pk=pk, project_id=project_id, actor=request.user + ) + issue_activity.delay( + type="comment.activity.deleted", + requested_data=json.dumps({"comment_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=json.dumps( + IssueCommentSerializer(comment).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + ) + comment.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class IssueReactionPublicViewSet(BaseViewSet): @@ -1648,93 +1515,94 @@ class IssueReactionPublicViewSet(BaseViewSet): model = IssueReaction def get_queryset(self): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.reactions: - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .order_by("-created_at") - .distinct() - ) - else: - return IssueReaction.objects.none() - - def create(self, request, slug, project_id, issue_id): try: project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.reactions: - return Response( - {"error": "Reactions are not enabled for this project board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = IssueReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, issue_id=issue_id, actor=request.user + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .order_by("-created_at") + .distinct() ) - if not ProjectMember.objects.filter( - project_id=project_id, - member=request.user, - ).exists(): - # Add the user for workspace tracking - _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, - member=request.user, - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return IssueReaction.objects.none() except ProjectDeployBoard.DoesNotExist: + return IssueReaction.objects.none() + + def create(self, request, slug, project_id, issue_id): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.reactions: return Response( - {"error": "Project board does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Reactions are not enabled for this project board"}, status=status.HTTP_400_BAD_REQUEST, ) - def destroy(self, request, slug, project_id, issue_id, reaction_code): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + serializer = IssueReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, issue_id=issue_id, actor=request.user ) - - if not project_deploy_board.reactions: - return Response( - {"error": "Reactions are not enabled for this project board"}, - status=status.HTTP_400_BAD_REQUEST, + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, ) - issue_reaction = IssueReaction.objects.get( - workspace__slug=slug, - issue_id=issue_id, - reaction=reaction_code, - actor=request.user, + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), ) - issue_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except IssueReaction.DoesNotExist: - return Response( - {"error": "Issue reaction does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, reaction_code): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.reactions: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Reactions are not enabled for this project board"}, status=status.HTTP_400_BAD_REQUEST, ) + issue_reaction = IssueReaction.objects.get( + workspace__slug=slug, + issue_id=issue_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(issue_reaction.id), + } + ), + epoch=int(timezone.now().timestamp()), + ) + issue_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class CommentReactionPublicViewSet(BaseViewSet): @@ -1742,151 +1610,256 @@ class CommentReactionPublicViewSet(BaseViewSet): model = CommentReaction def get_queryset(self): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.reactions: - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(comment_id=self.kwargs.get("comment_id")) - .order_by("-created_at") - .distinct() - ) - else: - return CommentReaction.objects.none() - - def create(self, request, slug, project_id, comment_id): try: project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.reactions: - return Response( - {"error": "Reactions are not enabled for this board"}, - status=status.HTTP_400_BAD_REQUEST, + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(comment_id=self.kwargs.get("comment_id")) + .order_by("-created_at") + .distinct() ) - - serializer = CommentReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, comment_id=comment_id, actor=request.user - ) - if not ProjectMember.objects.filter( - project_id=project_id, member=request.user - ).exists(): - # Add the user for workspace tracking - _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, - member=request.user, - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return CommentReaction.objects.none() except ProjectDeployBoard.DoesNotExist: + return CommentReaction.objects.none() + + def create(self, request, slug, project_id, comment_id): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.reactions: return Response( - {"error": "Project board does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Reactions are not enabled for this board"}, status=status.HTTP_400_BAD_REQUEST, ) - def destroy(self, request, slug, project_id, comment_id, reaction_code): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + serializer = CommentReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, comment_id=comment_id, actor=request.user ) - if not project_deploy_board.reactions: - return Response( - {"error": "Reactions are not enabled for this board"}, - status=status.HTTP_400_BAD_REQUEST, + if not ProjectMember.objects.filter( + project_id=project_id, member=request.user + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, ) - - comment_reaction = CommentReaction.objects.get( - project_id=project_id, - workspace__slug=slug, - comment_id=comment_id, - reaction=reaction_code, - actor=request.user, - ) - comment_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except CommentReaction.DoesNotExist: - return Response( - {"error": "Comment reaction does not exist"}, - status=status.HTTP_400_BAD_REQUEST, + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), ) - except Exception as e: - capture_exception(e) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, comment_id, reaction_code): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + if not project_deploy_board.reactions: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Reactions are not enabled for this board"}, status=status.HTTP_400_BAD_REQUEST, ) + comment_reaction = CommentReaction.objects.get( + project_id=project_id, + workspace__slug=slug, + comment_id=comment_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="comment_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(comment_reaction.id), + "comment_id": str(comment_id), + } + ), + epoch=int(timezone.now().timestamp()), + ) + comment_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + class IssueVotePublicViewSet(BaseViewSet): model = IssueVote serializer_class = IssueVoteSerializer def get_queryset(self): - return ( + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.votes: + return ( + super() + .get_queryset() + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + ) + return IssueVote.objects.none() + except ProjectDeployBoard.DoesNotExist: + return IssueVote.objects.none() + + def create(self, request, slug, project_id, issue_id): + issue_vote, _ = IssueVote.objects.get_or_create( + actor_id=request.user.id, + project_id=project_id, + issue_id=issue_id, + ) + # Add the user for workspace tracking + if not ProjectMember.objects.filter( + project_id=project_id, member=request.user + ).exists(): + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + issue_vote.vote = request.data.get("vote", 1) + issue_vote.save() + issue_activity.delay( + type="issue_vote.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + serializer = IssueVoteSerializer(issue_vote) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def destroy(self, request, slug, project_id, issue_id): + issue_vote = IssueVote.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + actor_id=request.user.id, + ) + issue_activity.delay( + type="issue_vote.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "vote": str(issue_vote.vote), + "identifier": str(issue_vote.id), + } + ), + epoch=int(timezone.now().timestamp()), + ) + issue_vote.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class IssueRelationViewSet(BaseViewSet): + serializer_class = IssueRelationSerializer + model = IssueRelation + permission_classes = [ + ProjectEntityPermission, + ] + + def get_queryset(self): + return self.filter_queryset( super() .get_queryset() - .filter(issue_id=self.kwargs.get("issue_id")) .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(project__project_projectmember__member=self.request.user) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .distinct() ) def create(self, request, slug, project_id, issue_id): - try: - issue_vote, _ = IssueVote.objects.get_or_create( - actor_id=request.user.id, - project_id=project_id, - issue_id=issue_id, - ) - # Add the user for workspace tracking - if not ProjectMember.objects.filter( - project_id=project_id, member=request.user - ).exists(): - _ = ProjectPublicMember.objects.get_or_create( + related_list = request.data.get("related_list", []) + relation = request.data.get("relation", None) + project = Project.objects.get(pk=project_id) + + issue_relation = IssueRelation.objects.bulk_create( + [ + IssueRelation( + issue_id=related_issue["issue"], + related_issue_id=related_issue["related_issue"], + relation_type=related_issue["relation_type"], project_id=project_id, - member=request.user, + workspace_id=project.workspace_id, + created_by=request.user, + updated_by=request.user, ) - issue_vote.vote = request.data.get("vote", 1) - issue_vote.save() - serializer = IssueVoteSerializer(issue_vote) - return Response(serializer.data, status=status.HTTP_201_CREATED) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + for related_issue in related_list + ], + batch_size=10, + ignore_conflicts=True, + ) - def destroy(self, request, slug, project_id, issue_id): - try: - issue_vote = IssueVote.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - actor_id=request.user.id, + issue_activity.delay( + type="issue_relation.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + + if relation == "blocking": + return Response( + RelatedIssueSerializer(issue_relation, many=True).data, + status=status.HTTP_201_CREATED, ) - issue_vote.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except Exception as e: - capture_exception(e) + else: return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + IssueRelationSerializer(issue_relation, many=True).data, + status=status.HTTP_201_CREATED, ) + def destroy(self, request, slug, project_id, issue_id, pk): + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk + ) + current_instance = json.dumps( + IssueRelationSerializer(issue_relation).data, + cls=DjangoJSONEncoder, + ) + issue_relation.delete() + issue_activity.delay( + type="issue_relation.activity.deleted", + requested_data=json.dumps({"related_list": None}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + class IssueRetrievePublicEndpoint(BaseAPIView): permission_classes = [ @@ -1894,22 +1867,11 @@ class IssueRetrievePublicEndpoint(BaseAPIView): ] def get(self, request, slug, project_id, issue_id): - try: - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=issue_id - ) - serializer = IssuePublicSerializer(issue) - return Response(serializer.data, status=status.HTTP_200_OK) - except Issue.DoesNotExist: - return Response( - {"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - print(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=issue_id + ) + serializer = IssuePublicSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) class ProjectIssuesPublicEndpoint(BaseAPIView): @@ -1918,144 +1880,366 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): ] def get(self, request, slug, project_id): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) - filters = issue_filters(request.query_params, "GET") + filters = issue_filters(request.query_params, "GET") - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - order_by_param = request.GET.get("order_by", "-created_at") + order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = ( - Issue.issue_objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(project_id=project_id) - .filter(workspace__slug=slug) - .select_related("project", "workspace", "state", "parent") - .prefetch_related("assignees", "labels") - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) - ) - .filter(**filters) - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate(module_id=F("issue_module__module_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") + issue_queryset = ( + Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .select_related("project", "workspace", "state", "parent") + .prefetch_related("assignees", "labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") + ) + .prefetch_related( + Prefetch( + "votes", + queryset=IssueVote.objects.select_related("actor"), ) ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order if order_by_param == "priority" else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issues = IssuePublicSerializer(issue_queryset, many=True).data + + state_group_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + states = ( + State.objects.filter( + ~Q(name="Triage"), + workspace__slug=slug, + project_id=project_id, + ) + .annotate( + custom_order=Case( + *[ + When(group=value, then=Value(index)) + for index, value in enumerate(state_group_order) + ], + default=Value(len(state_group_order)), + output_field=IntegerField(), + ), + ) + .values("name", "group", "color", "id") + .order_by("custom_order", "sequence") + ) + + labels = Label.objects.filter( + workspace__slug=slug, project_id=project_id + ).values("id", "name", "color", "parent") + + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + issues = group_results(issues, group_by) + + return Response( + { + "issues": issues, + "states": states, + "labels": labels, + }, + status=status.HTTP_200_OK, + ) + + +class IssueDraftViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + serializer_class = IssueFlatSerializer + model = Issue + + def get_queryset(self): + return ( + Issue.objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(is_draft=True) + .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"), + ) + ) + ) - issues = IssuePublicSerializer(issue_queryset, many=True).data + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") - states = State.objects.filter( - workspace__slug=slug, project_id=project_id - ).values("name", "group", "color", "id") + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - labels = Label.objects.filter( - workspace__slug=slug, project_id=project_id - ).values("id", "name", "color", "parent") + order_by_param = request.GET.get("order_by", "-created_at") - ## Grouping the results - group_by = request.GET.get("group_by", False) - if group_by: - issues = group_results(issues, group_by) + issue_queryset = ( + self.get_queryset() + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order if order_by_param == "priority" else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issues = IssueLiteSerializer(issue_queryset, many=True).data + + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + grouped_results = group_results(issues, group_by) return Response( - { - "issues": issues, - "states": states, - "labels": labels, - }, + grouped_results, status=status.HTTP_200_OK, ) - except ProjectDeployBoard.DoesNotExist: - return Response( - {"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND + + return Response(issues, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id): + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save(is_draft=True) + + # Track the issue + issue_activity.delay( + type="issue_draft.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, pk): + issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + serializer = IssueSerializer(issue, data=request.data, partial=True) + + if serializer.is_valid(): + if request.data.get("is_draft") is not None and not request.data.get( + "is_draft" + ): + serializer.save(created_at=timezone.now(), updated_at=timezone.now()) + else: + serializer.save() + issue_activity.delay( + type="issue_draft.activity.updated", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + IssueSerializer(issue).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def retrieve(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True + ) + return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) + + def destroy(self, request, slug, project_id, pk=None): + issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) + current_instance = json.dumps( + IssueSerializer(current_instance).data, cls=DjangoJSONEncoder + ) + issue.delete() + issue_activity.delay( + type="issue_draft.activity.deleted", + requested_data=json.dumps({"issue_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=current_instance, + epoch=int(timezone.now().timestamp()), + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 1cd741f8456..48f892764c5 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -2,6 +2,7 @@ import json # Django Imports +from django.utils import timezone from django.db import IntegrityError from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q from django.core import serializers @@ -39,6 +40,7 @@ from plane.utils.issue_filters import issue_filters from plane.utils.analytics_plot import burndown_plot + class ModuleViewSet(BaseViewSet): model = Module permission_classes = [ @@ -77,172 +79,209 @@ def get_queryset(self): queryset=ModuleLink.objects.select_related("module", "created_by"), ) ) - .annotate(total_issues=Count("issue_module")) + .annotate( + total_issues=Count( + "issue_module", + filter=Q( + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ), + ) .annotate( completed_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="completed"), + filter=Q( + issue_module__issue__state__group="completed", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .annotate( cancelled_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="cancelled"), + filter=Q( + issue_module__issue__state__group="cancelled", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .annotate( started_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="started"), + filter=Q( + issue_module__issue__state__group="started", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .annotate( unstarted_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="unstarted"), + filter=Q( + issue_module__issue__state__group="unstarted", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .annotate( backlog_issues=Count( "issue_module__issue__state__group", - filter=Q(issue_module__issue__state__group="backlog"), + filter=Q( + issue_module__issue__state__group="backlog", + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), ) ) .order_by(order_by, "name") ) - def perform_destroy(self, instance): - module_issues = list( - ModuleIssue.objects.filter(module_id=self.kwargs.get("pk")).values_list( - "issue", flat=True - ) - ) - issue_activity.delay( - type="module.activity.deleted", - requested_data=json.dumps( - { - "module_id": str(self.kwargs.get("pk")), - "issues": [str(issue_id) for issue_id in module_issues], - } - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, + def create(self, request, slug, project_id): + project = Project.objects.get(workspace__slug=slug, pk=project_id) + serializer = ModuleWriteSerializer( + data=request.data, context={"project": project} ) - return super().perform_destroy(instance) + if serializer.is_valid(): + serializer.save() - def create(self, request, slug, project_id): - try: - project = Project.objects.get(workspace__slug=slug, pk=project_id) - serializer = ModuleWriteSerializer( - data=request.data, context={"project": project} - ) + module = Module.objects.get(pk=serializer.data["id"]) + serializer = ModuleSerializer(module) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def retrieve(self, request, slug, project_id, pk): + queryset = self.get_queryset().get(pk=pk) - except Project.DoesNotExist: - return Response( - {"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND + assignee_distribution = ( + Issue.objects.filter( + issue_module__module_id=pk, + workspace__slug=slug, + project_id=project_id, ) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"name": "The module name is already taken"}, - status=status.HTTP_410_GONE, + .annotate(first_name=F("assignees__first_name")) + .annotate(last_name=F("assignees__last_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(display_name=F("assignees__display_name")) + .annotate(avatar=F("assignees__avatar")) + .values("first_name", "last_name", "assignee_id", "avatar", "display_name") + .annotate( + total_issues=Count( + "assignee_id", + filter=Q( + archived_at__isnull=True, + is_draft=False, + ), ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, ) - - def retrieve(self, request, slug, project_id, pk): - try: - queryset = self.get_queryset().get(pk=pk) - - assignee_distribution = ( - Issue.objects.filter( - issue_module__module_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(first_name=F("assignees__first_name")) - .annotate(last_name=F("assignees__last_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(display_name=F("assignees__display_name")) - .annotate(avatar=F("assignees__avatar")) - .values("first_name", "last_name", "assignee_id", "avatar", "display_name") - .annotate(total_issues=Count("assignee_id")) - .annotate( - completed_issues=Count( - "assignee_id", - filter=Q(completed_at__isnull=False), - ) + .annotate( + completed_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) - .annotate( - pending_issues=Count( - "assignee_id", - filter=Q(completed_at__isnull=True), - ) + ) + .annotate( + pending_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) - .order_by("first_name", "last_name") ) + .order_by("first_name", "last_name") + ) - label_distribution = ( - Issue.objects.filter( - issue_module__module_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate(total_issues=Count("label_id")) - .annotate( - completed_issues=Count( - "label_id", - filter=Q(completed_at__isnull=False), - ) + label_distribution = ( + Issue.objects.filter( + issue_module__module_id=pk, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_issues=Count( + "label_id", + filter=Q( + archived_at__isnull=True, + is_draft=False, + ), + ), + ) + .annotate( + completed_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), ) - .annotate( - pending_issues=Count( - "label_id", - filter=Q(completed_at__isnull=True), - ) + ) + .annotate( + pending_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), ) - .order_by("label_name") ) + .order_by("label_name") + ) - data = ModuleSerializer(queryset).data - data["distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } + data = ModuleSerializer(queryset).data + data["distribution"] = { + "assignees": assignee_distribution, + "labels": label_distribution, + "completion_chart": {}, + } - if queryset.start_date and queryset.target_date: - data["distribution"]["completion_chart"] = burndown_plot( - queryset=queryset, slug=slug, project_id=project_id, module_id=pk - ) - - return Response( - data, - status=status.HTTP_200_OK, + if queryset.start_date and queryset.target_date: + data["distribution"]["completion_chart"] = burndown_plot( + queryset=queryset, slug=slug, project_id=project_id, module_id=pk ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response( + data, + status=status.HTTP_200_OK, + ) + + def destroy(self, request, slug, project_id, pk): + module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=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), + "issues": [str(issue_id) for issue_id in module_issues], + } + ), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + return Response(status=status.HTTP_204_NO_CONTENT) class ModuleIssueViewSet(BaseViewSet): @@ -264,22 +303,6 @@ def perform_create(self, serializer): module_id=self.kwargs.get("module_id"), ) - def perform_destroy(self, instance): - issue_activity.delay( - type="module.activity.deleted", - requested_data=json.dumps( - { - "module_id": str(self.kwargs.get("module_id")), - "issues": [str(instance.issue_id)], - } - ), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("pk", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - ) - return super().perform_destroy(instance) - def get_queryset(self): return self.filter_queryset( super() @@ -305,154 +328,162 @@ def get_queryset(self): @method_decorator(gzip_page) def list(self, request, slug, project_id, module_id): - try: - order_by = request.GET.get("order_by", "created_at") - group_by = request.GET.get("group_by", False) - filters = issue_filters(request.query_params, "GET") - issues = ( - Issue.issue_objects.filter(issue_module__module_id=module_id) - .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate(bridge_id=F("issue_module__id")) - .filter(project_id=project_id) - .filter(workspace__slug=slug) - .select_related("project") - .select_related("workspace") - .select_related("state") - .select_related("parent") - .prefetch_related("assignees") - .prefetch_related("labels") - .order_by(order_by) - .filter(**filters) - .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") - ) + order_by = request.GET.get("order_by", "created_at") + group_by = request.GET.get("group_by", False) + sub_group_by = request.GET.get("sub_group_by", False) + filters = issue_filters(request.query_params, "GET") + issues = ( + Issue.issue_objects.filter(issue_module__module_id=module_id) + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) + .annotate(bridge_id=F("issue_module__id")) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .order_by(order_by) + .filter(**filters) + .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") + ) + ) + issues_data = IssueStateSerializer(issues, many=True).data - issues_data = IssueStateSerializer(issues, many=True).data - - if group_by: - return Response( - group_results(issues_data, group_by), - status=status.HTTP_200_OK, - ) - + if sub_group_by and sub_group_by == group_by: return Response( - issues_data, - status=status.HTTP_200_OK, + {"error": "Group by and sub group by cannot be same"}, + status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) + + if group_by: + grouped_results = group_results(issues_data, group_by, sub_group_by) return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + grouped_results, + status=status.HTTP_200_OK, ) + return Response( + issues_data, status=status.HTTP_200_OK + ) + def create(self, request, slug, project_id, module_id): - try: - issues = request.data.get("issues", []) - if not len(issues): - return Response( - {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST - ) - module = Module.objects.get( - workspace__slug=slug, project_id=project_id, pk=module_id + issues = request.data.get("issues", []) + if not len(issues): + return Response( + {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST ) + module = Module.objects.get( + workspace__slug=slug, project_id=project_id, pk=module_id + ) - module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues)) - - update_module_issue_activity = [] - records_to_update = [] - record_to_create = [] - - for issue in issues: - module_issue = [ - module_issue - for module_issue in module_issues - if str(module_issue.issue_id) in issues - ] - - if len(module_issue): - if module_issue[0].module_id != module_id: - update_module_issue_activity.append( - { - "old_module_id": str(module_issue[0].module_id), - "new_module_id": str(module_id), - "issue_id": str(module_issue[0].issue_id), - } - ) - module_issue[0].module_id = module_id - records_to_update.append(module_issue[0]) - else: - record_to_create.append( - ModuleIssue( - module=module, - issue_id=issue, - project_id=project_id, - workspace=module.workspace, - created_by=request.user, - updated_by=request.user, - ) + module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues)) + + update_module_issue_activity = [] + records_to_update = [] + record_to_create = [] + + for issue in issues: + module_issue = [ + module_issue + for module_issue in module_issues + if str(module_issue.issue_id) in issues + ] + + if len(module_issue): + if module_issue[0].module_id != module_id: + update_module_issue_activity.append( + { + "old_module_id": str(module_issue[0].module_id), + "new_module_id": str(module_id), + "issue_id": str(module_issue[0].issue_id), + } ) + module_issue[0].module_id = module_id + records_to_update.append(module_issue[0]) + else: + record_to_create.append( + ModuleIssue( + module=module, + issue_id=issue, + project_id=project_id, + workspace=module.workspace, + created_by=request.user, + updated_by=request.user, + ) + ) - ModuleIssue.objects.bulk_create( - record_to_create, - batch_size=10, - ignore_conflicts=True, - ) + ModuleIssue.objects.bulk_create( + record_to_create, + batch_size=10, + ignore_conflicts=True, + ) - ModuleIssue.objects.bulk_update( - records_to_update, - ["module"], - batch_size=10, - ) + ModuleIssue.objects.bulk_update( + records_to_update, + ["module"], + batch_size=10, + ) - # Capture Issue Activity - issue_activity.delay( - 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)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "updated_module_issues": update_module_issue_activity, - "created_module_issues": serializers.serialize( - "json", record_to_create - ), - } - ), - ) + # Capture Issue Activity + issue_activity.delay( + 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)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "updated_module_issues": update_module_issue_activity, + "created_module_issues": serializers.serialize( + "json", record_to_create + ), + } + ), + epoch=int(timezone.now().timestamp()), + ) - return Response( - ModuleIssueSerializer(self.get_queryset(), many=True).data, - status=status.HTTP_200_OK, - ) - except Module.DoesNotExist: - return Response( - {"error": "Module Does not exists"}, status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response( + ModuleIssueSerializer(self.get_queryset(), many=True).data, + status=status.HTTP_200_OK, + ) + + def destroy(self, request, slug, project_id, module_id, pk): + module_issue = ModuleIssue.objects.get( + workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk + ) + module_issue.delete() + issue_activity.delay( + type="module.activity.deleted", + requested_data=json.dumps( + { + "module_id": str(module_id), + "issues": [str(module_issue.issue_id)], + } + ), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + return Response(status=status.HTTP_204_NO_CONTENT) class ModuleLinkViewSet(BaseViewSet): @@ -483,7 +514,6 @@ def get_queryset(self): class ModuleFavoriteViewSet(BaseViewSet): - serializer_class = ModuleFavoriteSerializer model = ModuleFavorite @@ -497,49 +527,18 @@ def get_queryset(self): ) def create(self, request, slug, project_id): - try: - serializer = ModuleFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user, project_id=project_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "The module is already added to favorites"}, - status=status.HTTP_410_GONE, - ) - else: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = ModuleFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user, project_id=project_id) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, slug, project_id, module_id): - try: - module_favorite = ModuleFavorite.objects.get( - project=project_id, - user=request.user, - workspace__slug=slug, - module_id=module_id, - ) - module_favorite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except ModuleFavorite.DoesNotExist: - return Response( - {"error": "Module is not in favorites"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + module_favorite = ModuleFavorite.objects.get( + project=project_id, + user=request.user, + workspace__slug=slug, + module_id=module_id, + ) + module_favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/api/views/notification.py index 75b94f034bd..978c01bac34 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/api/views/notification.py @@ -36,328 +36,239 @@ def get_queryset(self): ) def list(self, request, slug): - try: - snoozed = request.GET.get("snoozed", "false") - archived = request.GET.get("archived", "false") - read = request.GET.get("read", "true") - - # Filter type - type = request.GET.get("type", "all") - - notifications = ( - Notification.objects.filter( - workspace__slug=slug, receiver_id=request.user.id - ) - .select_related("workspace", "project", "triggered_by", "receiver") - .order_by("snoozed_till", "-created_at") + # Get query parameters + snoozed = request.GET.get("snoozed", "false") + archived = request.GET.get("archived", "false") + read = request.GET.get("read", "true") + type = request.GET.get("type", "all") + + notifications = ( + Notification.objects.filter( + workspace__slug=slug, receiver_id=request.user.id ) + .select_related("workspace", "project", "triggered_by", "receiver") + .order_by("snoozed_till", "-created_at") + ) - # Filter for snoozed notifications - if snoozed == "false": - notifications = notifications.filter( - Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), - ) - - if snoozed == "true": - notifications = notifications.filter( - Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False) - ) - - if read == "false": - notifications = notifications.filter(read_at__isnull=True) - - # Filter for archived or unarchive - if archived == "false": - notifications = notifications.filter(archived_at__isnull=True) - - if archived == "true": - notifications = notifications.filter(archived_at__isnull=False) - - # Subscribed issues - if type == "watching": - issue_ids = IssueSubscriber.objects.filter( - workspace__slug=slug, subscriber_id=request.user.id - ).values_list("issue_id", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) - - # Assigned Issues - if type == "assigned": - issue_ids = IssueAssignee.objects.filter( - workspace__slug=slug, assignee_id=request.user.id - ).values_list("issue_id", flat=True) + # Filters based on query parameters + snoozed_filters = { + "true": Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False), + "false": Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + } + + notifications = notifications.filter(snoozed_filters[snoozed]) + + archived_filters = { + "true": Q(archived_at__isnull=False), + "false": Q(archived_at__isnull=True), + } + + notifications = notifications.filter(archived_filters[archived]) + + if read == "false": + notifications = notifications.filter(read_at__isnull=True) + + # Subscribed issues + if type == "watching": + issue_ids = IssueSubscriber.objects.filter( + workspace__slug=slug, subscriber_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_identifier__in=issue_ids) + + # Assigned Issues + if type == "assigned": + issue_ids = IssueAssignee.objects.filter( + workspace__slug=slug, assignee_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_identifier__in=issue_ids) + + # Created issues + if type == "created": + if WorkspaceMember.objects.filter( + workspace__slug=slug, member=request.user, role__lt=15 + ).exists(): + notifications = Notification.objects.none() + else: + issue_ids = Issue.objects.filter( + workspace__slug=slug, created_by=request.user + ).values_list("pk", flat=True) notifications = notifications.filter(entity_identifier__in=issue_ids) - # Created issues - if type == "created": - if WorkspaceMember.objects.filter( - workspace__slug=slug, member=request.user, role__lt=15 - ).exists(): - notifications = Notification.objects.none() - else: - issue_ids = Issue.objects.filter( - workspace__slug=slug, created_by=request.user - ).values_list("pk", flat=True) - notifications = notifications.filter( - entity_identifier__in=issue_ids - ) - - # Pagination - if request.GET.get("per_page", False) and request.GET.get("cursor", False): - return self.paginate( - request=request, - queryset=(notifications), - on_results=lambda notifications: NotificationSerializer( - notifications, many=True - ).data, - ) - - serializer = NotificationSerializer(notifications, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + # Pagination + if request.GET.get("per_page", False) and request.GET.get("cursor", False): + return self.paginate( + request=request, + queryset=(notifications), + on_results=lambda notifications: NotificationSerializer( + notifications, many=True + ).data, ) + serializer = NotificationSerializer(notifications, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + def partial_update(self, request, slug, pk): - try: - notification = Notification.objects.get( - workspace__slug=slug, pk=pk, receiver=request.user - ) - # Only read_at and snoozed_till can be updated - notification_data = { - "snoozed_till": request.data.get("snoozed_till", None), - } - serializer = NotificationSerializer( - notification, data=notification_data, partial=True - ) + notification = Notification.objects.get( + workspace__slug=slug, pk=pk, receiver=request.user + ) + # Only read_at and snoozed_till can be updated + notification_data = { + "snoozed_till": request.data.get("snoozed_till", None), + } + serializer = NotificationSerializer( + notification, data=notification_data, partial=True + ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Notification.DoesNotExist: - return Response( - {"error": "Notification does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def mark_read(self, request, slug, pk): - try: - notification = Notification.objects.get( - receiver=request.user, workspace__slug=slug, pk=pk - ) - notification.read_at = timezone.now() - notification.save() - serializer = NotificationSerializer(notification) - return Response(serializer.data, status=status.HTTP_200_OK) - except Notification.DoesNotExist: - return Response( - {"error": "Notification does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + notification = Notification.objects.get( + receiver=request.user, workspace__slug=slug, pk=pk + ) + notification.read_at = timezone.now() + notification.save() + serializer = NotificationSerializer(notification) + return Response(serializer.data, status=status.HTTP_200_OK) def mark_unread(self, request, slug, pk): - try: - notification = Notification.objects.get( - receiver=request.user, workspace__slug=slug, pk=pk - ) - notification.read_at = None - notification.save() - serializer = NotificationSerializer(notification) - return Response(serializer.data, status=status.HTTP_200_OK) - except Notification.DoesNotExist: - return Response( - {"error": "Notification does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + notification = Notification.objects.get( + receiver=request.user, workspace__slug=slug, pk=pk + ) + notification.read_at = None + notification.save() + serializer = NotificationSerializer(notification) + return Response(serializer.data, status=status.HTTP_200_OK) def archive(self, request, slug, pk): - try: - notification = Notification.objects.get( - receiver=request.user, workspace__slug=slug, pk=pk - ) - notification.archived_at = timezone.now() - notification.save() - serializer = NotificationSerializer(notification) - return Response(serializer.data, status=status.HTTP_200_OK) - except Notification.DoesNotExist: - return Response( - {"error": "Notification does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + notification = Notification.objects.get( + receiver=request.user, workspace__slug=slug, pk=pk + ) + notification.archived_at = timezone.now() + notification.save() + serializer = NotificationSerializer(notification) + return Response(serializer.data, status=status.HTTP_200_OK) def unarchive(self, request, slug, pk): - try: - notification = Notification.objects.get( - receiver=request.user, workspace__slug=slug, pk=pk - ) - notification.archived_at = None - notification.save() - serializer = NotificationSerializer(notification) - return Response(serializer.data, status=status.HTTP_200_OK) - except Notification.DoesNotExist: - return Response( - {"error": "Notification does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + notification = Notification.objects.get( + receiver=request.user, workspace__slug=slug, pk=pk + ) + notification.archived_at = None + notification.save() + serializer = NotificationSerializer(notification) + return Response(serializer.data, status=status.HTTP_200_OK) class UnreadNotificationEndpoint(BaseAPIView): def get(self, request, slug): - try: - # Watching Issues Count - watching_issues_count = Notification.objects.filter( - workspace__slug=slug, - receiver_id=request.user.id, - read_at__isnull=True, - archived_at__isnull=True, - entity_identifier__in=IssueSubscriber.objects.filter( - workspace__slug=slug, subscriber_id=request.user.id - ).values_list("issue_id", flat=True), - ).count() - - # My Issues Count - my_issues_count = Notification.objects.filter( - workspace__slug=slug, - receiver_id=request.user.id, - read_at__isnull=True, - archived_at__isnull=True, - entity_identifier__in=IssueAssignee.objects.filter( - workspace__slug=slug, assignee_id=request.user.id - ).values_list("issue_id", flat=True), - ).count() - - # Created Issues Count - created_issues_count = Notification.objects.filter( - workspace__slug=slug, - receiver_id=request.user.id, - read_at__isnull=True, - archived_at__isnull=True, - entity_identifier__in=Issue.objects.filter( - workspace__slug=slug, created_by=request.user - ).values_list("pk", flat=True), - ).count() - - return Response( - { - "watching_issues": watching_issues_count, - "my_issues": my_issues_count, - "created_issues": created_issues_count, - }, - status=status.HTTP_200_OK, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + # Watching Issues Count + watching_issues_count = Notification.objects.filter( + workspace__slug=slug, + receiver_id=request.user.id, + read_at__isnull=True, + archived_at__isnull=True, + entity_identifier__in=IssueSubscriber.objects.filter( + workspace__slug=slug, subscriber_id=request.user.id + ).values_list("issue_id", flat=True), + ).count() + + # My Issues Count + my_issues_count = Notification.objects.filter( + workspace__slug=slug, + receiver_id=request.user.id, + read_at__isnull=True, + archived_at__isnull=True, + entity_identifier__in=IssueAssignee.objects.filter( + workspace__slug=slug, assignee_id=request.user.id + ).values_list("issue_id", flat=True), + ).count() + + # Created Issues Count + created_issues_count = Notification.objects.filter( + workspace__slug=slug, + receiver_id=request.user.id, + read_at__isnull=True, + archived_at__isnull=True, + entity_identifier__in=Issue.objects.filter( + workspace__slug=slug, created_by=request.user + ).values_list("pk", flat=True), + ).count() + + return Response( + { + "watching_issues": watching_issues_count, + "my_issues": my_issues_count, + "created_issues": created_issues_count, + }, + status=status.HTTP_200_OK, + ) class MarkAllReadNotificationViewSet(BaseViewSet): def create(self, request, slug): - try: - snoozed = request.data.get("snoozed", False) - archived = request.data.get("archived", False) - type = request.data.get("type", "all") - - notifications = ( - Notification.objects.filter( - workspace__slug=slug, - receiver_id=request.user.id, - read_at__isnull=True, - ) - .select_related("workspace", "project", "triggered_by", "receiver") - .order_by("snoozed_till", "-created_at") + snoozed = request.data.get("snoozed", False) + archived = request.data.get("archived", False) + type = request.data.get("type", "all") + + notifications = ( + Notification.objects.filter( + workspace__slug=slug, + receiver_id=request.user.id, + read_at__isnull=True, ) + .select_related("workspace", "project", "triggered_by", "receiver") + .order_by("snoozed_till", "-created_at") + ) - # Filter for snoozed notifications - if snoozed: - notifications = notifications.filter( - Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False) - ) - else: - notifications = notifications.filter( - Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), - ) + # Filter for snoozed notifications + if snoozed: + notifications = notifications.filter( + Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False) + ) + else: + notifications = notifications.filter( + Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + ) - # Filter for archived or unarchive - if archived: - notifications = notifications.filter(archived_at__isnull=False) + # Filter for archived or unarchive + if archived: + notifications = notifications.filter(archived_at__isnull=False) + else: + notifications = notifications.filter(archived_at__isnull=True) + + # Subscribed issues + if type == "watching": + issue_ids = IssueSubscriber.objects.filter( + workspace__slug=slug, subscriber_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_identifier__in=issue_ids) + + # Assigned Issues + if type == "assigned": + issue_ids = IssueAssignee.objects.filter( + workspace__slug=slug, assignee_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_identifier__in=issue_ids) + + # Created issues + if type == "created": + if WorkspaceMember.objects.filter( + workspace__slug=slug, member=request.user, role__lt=15 + ).exists(): + notifications = Notification.objects.none() else: - notifications = notifications.filter(archived_at__isnull=True) - - # Subscribed issues - if type == "watching": - issue_ids = IssueSubscriber.objects.filter( - workspace__slug=slug, subscriber_id=request.user.id - ).values_list("issue_id", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) - - # Assigned Issues - if type == "assigned": - issue_ids = IssueAssignee.objects.filter( - workspace__slug=slug, assignee_id=request.user.id - ).values_list("issue_id", flat=True) + issue_ids = Issue.objects.filter( + workspace__slug=slug, created_by=request.user + ).values_list("pk", flat=True) notifications = notifications.filter(entity_identifier__in=issue_ids) - # Created issues - if type == "created": - if WorkspaceMember.objects.filter( - workspace__slug=slug, member=request.user, role__lt=15 - ).exists(): - notifications = Notification.objects.none() - else: - issue_ids = Issue.objects.filter( - workspace__slug=slug, created_by=request.user - ).values_list("pk", flat=True) - notifications = notifications.filter( - entity_identifier__in=issue_ids - ) - - updated_notifications = [] - for notification in notifications: - notification.read_at = timezone.now() - updated_notifications.append(notification) - Notification.objects.bulk_update( - updated_notifications, ["read_at"], batch_size=100 - ) - return Response({"message": "Successful"}, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + updated_notifications = [] + for notification in notifications: + notification.read_at = timezone.now() + updated_notifications.append(notification) + Notification.objects.bulk_update( + updated_notifications, ["read_at"], batch_size=100 + ) + return Response({"message": "Successful"}, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/oauth.py b/apiserver/plane/api/views/oauth.py index 184cba9517d..f0ea9acc99c 100644 --- a/apiserver/plane/api/views/oauth.py +++ b/apiserver/plane/api/views/oauth.py @@ -11,10 +11,10 @@ from rest_framework.response import Response from rest_framework import exceptions from rest_framework.permissions import AllowAny -from rest_framework.views import APIView from rest_framework_simplejwt.tokens import RefreshToken from rest_framework import status from sentry_sdk import capture_exception + # sso authentication from google.oauth2 import id_token from google.auth.transport import requests as google_auth_request @@ -112,7 +112,7 @@ def get_user_data(access_token: str) -> dict: url="https://api.github.com/user/emails", headers=headers ).json() - [ + _ = [ user_data.update({"email": item.get("email")}) for item in response if item.get("primary") is True @@ -146,7 +146,7 @@ def post(self, request): data = get_user_data(access_token) email = data.get("email", None) - if email == None: + if email is None: return Response( { "error": "Something went wrong. Please try again later or contact the support team." @@ -157,7 +157,6 @@ def post(self, request): if "@" in email: user = User.objects.get(email=email) email = data["email"] - channel = "email" mobile_number = uuid.uuid4().hex email_verified = True else: @@ -181,19 +180,16 @@ def post(self, request): user.last_active = timezone.now() user.last_login_time = timezone.now() user.last_login_ip = request.META.get("REMOTE_ADDR") - user.last_login_medium = f"oauth" + user.last_login_medium = "oauth" user.last_login_uagent = request.META.get("HTTP_USER_AGENT") user.is_email_verified = email_verified user.save() - serialized_user = UserSerializer(user).data - access_token, refresh_token = get_tokens_for_user(user) data = { "access_token": access_token, "refresh_token": refresh_token, - "user": serialized_user, } SocialLoginConnection.objects.update_or_create( @@ -235,7 +231,6 @@ def post(self, request): if "@" in email: email = data["email"] mobile_number = uuid.uuid4().hex - channel = "email" email_verified = True else: return Response( @@ -264,14 +259,11 @@ def post(self, request): user.last_login_uagent = request.META.get("HTTP_USER_AGENT") user.token_updated_at = timezone.now() user.save() - serialized_user = UserSerializer(user).data access_token, refresh_token = get_tokens_for_user(user) data = { "access_token": access_token, "refresh_token": refresh_token, - "user": serialized_user, - "permissions": [], } if settings.ANALYTICS_BASE_API: _ = requests.post( @@ -304,11 +296,3 @@ def post(self, request): }, ) return Response(data, status=status.HTTP_201_CREATED) - except Exception as e: - capture_exception(e) - return Response( - { - "error": "Something went wrong. Please try again later or contact the support team." - }, - status=status.HTTP_400_BAD_REQUEST, - ) diff --git a/apiserver/plane/api/views/page.py b/apiserver/plane/api/views/page.py index d9fad9eaa95..ca0927a519c 100644 --- a/apiserver/plane/api/views/page.py +++ b/apiserver/plane/api/views/page.py @@ -1,8 +1,7 @@ # Python imports -from datetime import timedelta, datetime, date +from datetime import timedelta, date # Django imports -from django.db import IntegrityError from django.db.models import Exists, OuterRef, Q, Prefetch from django.utils import timezone @@ -78,104 +77,82 @@ def perform_create(self, serializer): ) def create(self, request, slug, project_id): - try: - serializer = PageSerializer( - data=request.data, - context={"project_id": project_id, "owned_by_id": request.user.id}, - ) - - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer = PageSerializer( + data=request.data, + context={"project_id": project_id, "owned_by_id": request.user.id}, + ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def partial_update(self, request, slug, project_id, pk): - try: - page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) - # Only update access if the page owner is the requesting user - if ( - page.access != request.data.get("access", page.access) - and page.owned_by_id != request.user.id - ): - return Response( - { - "error": "Access cannot be updated since this page is owned by someone else" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - serializer = PageSerializer(page, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Page.DoesNotExist: - return Response( - {"error": "Page Does not exist"}, status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - capture_exception(e) + page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) + # Only update access if the page owner is the requesting user + if ( + page.access != request.data.get("access", page.access) + and page.owned_by_id != request.user.id + ): return Response( - {"error": "Something went wrong please try again later"}, + { + "error": "Access cannot be updated since this page is owned by someone else" + }, status=status.HTTP_400_BAD_REQUEST, ) + serializer = PageSerializer(page, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def list(self, request, slug, project_id): - try: - queryset = self.get_queryset() - page_view = request.GET.get("page_view", False) - - if not page_view: - return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST) - - # All Pages - if page_view == "all": - return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + queryset = self.get_queryset() + page_view = request.GET.get("page_view", False) + + if not page_view: + return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST) + + # All Pages + if page_view == "all": + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + # Recent pages + if page_view == "recent": + current_time = date.today() + day_before = current_time - timedelta(days=1) + todays_pages = queryset.filter(updated_at__date=date.today()) + yesterdays_pages = queryset.filter(updated_at__date=day_before) + earlier_this_week = queryset.filter( updated_at__date__range=( + (timezone.now() - timedelta(days=7)), + (timezone.now() - timedelta(days=2)), + )) + return Response( + { + "today": PageSerializer(todays_pages, many=True).data, + "yesterday": PageSerializer(yesterdays_pages, many=True).data, + "earlier_this_week": PageSerializer(earlier_this_week, many=True).data, + }, + status=status.HTTP_200_OK, + ) - # Recent pages - if page_view == "recent": - current_time = date.today() - day_before = current_time - timedelta(days=1) - todays_pages = queryset.filter(updated_at__date=date.today()) - yesterdays_pages = queryset.filter(updated_at__date=day_before) - earlier_this_week = queryset.filter( updated_at__date__range=( - (timezone.now() - timedelta(days=7)), - (timezone.now() - timedelta(days=2)), - )) - return Response( - { - "today": PageSerializer(todays_pages, many=True).data, - "yesterday": PageSerializer(yesterdays_pages, many=True).data, - "earlier_this_week": PageSerializer(earlier_this_week, many=True).data, - }, - status=status.HTTP_200_OK, - ) + # Favorite Pages + if page_view == "favorite": + queryset = queryset.filter(is_favorite=True) + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + # My pages + if page_view == "created_by_me": + queryset = queryset.filter(owned_by=request.user) + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) - # Favorite Pages - if page_view == "favorite": - queryset = queryset.filter(is_favorite=True) - return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) - - # My pages - if page_view == "created_by_me": - queryset = queryset.filter(owned_by=request.user) - return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + # Created by other Pages + if page_view == "created_by_other": + queryset = queryset.filter(~Q(owned_by=request.user), access=0) + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) - # Created by other Pages - if page_view == "created_by_other": - queryset = queryset.filter(~Q(owned_by=request.user), access=0) - return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + return Response({"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST) - return Response({"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) - return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST) class PageBlockViewSet(BaseViewSet): serializer_class = PageBlockSerializer @@ -225,53 +202,21 @@ def get_queryset(self): ) def create(self, request, slug, project_id): - try: - serializer = PageFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user, project_id=project_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "The page is already added to favorites"}, - status=status.HTTP_410_GONE, - ) - else: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = PageFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user, project_id=project_id) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, slug, project_id, page_id): - try: - page_favorite = PageFavorite.objects.get( - project=project_id, - user=request.user, - workspace__slug=slug, - page_id=page_id, - ) - page_favorite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except PageFavorite.DoesNotExist: - return Response( - {"error": "Page is not in favorites"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - + page_favorite = PageFavorite.objects.get( + project=project_id, + user=request.user, + workspace__slug=slug, + page_id=page_id, + ) + page_favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class CreateIssueFromPageBlockEndpoint(BaseAPIView): permission_classes = [ @@ -279,43 +224,32 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView): ] def post(self, request, slug, project_id, page_id, page_block_id): - try: - page_block = PageBlock.objects.get( - pk=page_block_id, - workspace__slug=slug, - project_id=project_id, - page_id=page_id, - ) - issue = Issue.objects.create( - name=page_block.name, - project_id=project_id, - description=page_block.description, - description_html=page_block.description_html, - description_stripped=page_block.description_stripped, - ) - _ = IssueAssignee.objects.create( - issue=issue, assignee=request.user, project_id=project_id - ) + page_block = PageBlock.objects.get( + pk=page_block_id, + workspace__slug=slug, + project_id=project_id, + page_id=page_id, + ) + issue = Issue.objects.create( + name=page_block.name, + project_id=project_id, + description=page_block.description, + description_html=page_block.description_html, + description_stripped=page_block.description_stripped, + ) + _ = IssueAssignee.objects.create( + issue=issue, assignee=request.user, project_id=project_id + ) - _ = IssueActivity.objects.create( - issue=issue, - actor=request.user, - project_id=project_id, - comment=f"created the issue from {page_block.name} block", - verb="created", - ) + _ = IssueActivity.objects.create( + issue=issue, + actor=request.user, + project_id=project_id, + comment=f"created the issue from {page_block.name} block", + verb="created", + ) - page_block.issue = issue - page_block.save() + page_block.issue = issue + page_block.save() - return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK) - except PageBlock.DoesNotExist: - return Response( - {"error": "Page Block does not exist"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 97b06fce581..37e491e8395 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -1,24 +1,19 @@ # Python imports import jwt +import boto3 from datetime import datetime # Django imports from django.core.exceptions import ValidationError from django.db import IntegrityError from django.db.models import ( + Prefetch, Q, Exists, OuterRef, - Func, F, - Max, - CharField, Func, Subquery, - Prefetch, - When, - Case, - Value, ) from django.core.validators import validate_email from django.conf import settings @@ -34,11 +29,11 @@ from .base import BaseViewSet, BaseAPIView from plane.api.serializers import ( ProjectSerializer, + ProjectListSerializer, ProjectMemberSerializer, ProjectDetailSerializer, ProjectMemberInviteSerializer, ProjectFavoriteSerializer, - IssueLiteSerializer, ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, ) @@ -47,6 +42,7 @@ ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, + ProjectLitePermission, ) from plane.db.models import ( @@ -71,16 +67,10 @@ ModuleMember, Inbox, ProjectDeployBoard, - Issue, - IssueReaction, - IssueLink, - IssueAttachment, - Label, + IssueProperty, ) from plane.bgtasks.project_invitation_task import project_invitation -from plane.utils.grouper import group_results -from plane.utils.issue_filters import issue_filters class ProjectViewSet(BaseViewSet): @@ -92,17 +82,11 @@ class ProjectViewSet(BaseViewSet): ] def get_serializer_class(self, *args, **kwargs): - if self.action == "update" or self.action == "partial_update": + if self.action in ["update", "partial_update"]: return ProjectSerializer return ProjectDetailSerializer def get_queryset(self): - subquery = ProjectFavorite.objects.filter( - user=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - return self.filter_queryset( super() .get_queryset() @@ -111,7 +95,15 @@ def get_queryset(self): .select_related( "workspace", "workspace__owner", "default_assignee", "project_lead" ) - .annotate(is_favorite=Exists(subquery)) + .annotate( + is_favorite=Exists( + ProjectFavorite.objects.filter( + user=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + ) + ) .annotate( is_member=Exists( ProjectMember.objects.filter( @@ -159,57 +151,40 @@ def get_queryset(self): ) def list(self, request, slug): - try: - is_favorite = request.GET.get("is_favorite", "all") - subquery = ProjectFavorite.objects.filter( - user=self.request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - sort_order_query = ProjectMember.objects.filter( - member=request.user, - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ).values("sort_order") - projects = ( - self.get_queryset() - .annotate(is_favorite=Exists(subquery)) - .annotate(sort_order=Subquery(sort_order_query)) - .order_by("sort_order", "name") - .annotate( - total_members=ProjectMember.objects.filter( - project_id=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - total_cycles=Cycle.objects.filter(project_id=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - total_modules=Module.objects.filter(project_id=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - ) + fields = [field for field in request.GET.get("fields", "").split(",") if field] - if is_favorite == "true": - projects = projects.filter(is_favorite=True) - if is_favorite == "false": - projects = projects.filter(is_favorite=False) - - return Response(ProjectDetailSerializer(projects, many=True).data) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + sort_order_query = ProjectMember.objects.filter( + member=request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ).values("sort_order") + projects = ( + self.get_queryset() + .annotate(sort_order=Subquery(sort_order_query)) + .prefetch_related( + Prefetch( + "project_projectmember", + queryset=ProjectMember.objects.filter( + workspace__slug=slug, + ).select_related("member"), + ) ) + .order_by("sort_order", "name") + ) + if request.GET.get("per_page", False) and request.GET.get("cursor", False): + return self.paginate( + request=request, + queryset=(projects), + on_results=lambda projects: ProjectListSerializer( + projects, many=True + ).data, + ) + + return Response( + ProjectListSerializer( + projects, many=True, fields=fields if fields else None + ).data + ) def create(self, request, slug): try: @@ -225,6 +200,11 @@ def create(self, request, slug): project_member = ProjectMember.objects.create( project_id=serializer.data["id"], member=request.user, role=20 ) + # Also create the issue property for the user + _ = IssueProperty.objects.create( + project_id=serializer.data["id"], + user=request.user, + ) if serializer.data["project_lead"] is not None and str( serializer.data["project_lead"] @@ -234,6 +214,11 @@ def create(self, request, slug): member_id=serializer.data["project_lead"], role=20, ) + # Also create the issue property for the user + IssueProperty.objects.create( + project_id=serializer.data["id"], + user_id=serializer.data["project_lead"], + ) # Default states states = [ @@ -286,12 +271,9 @@ def create(self, request, slug): ] ) - data = serializer.data - # Additional fields of the member - data["sort_order"] = project_member.sort_order - data["member_role"] = project_member.role - data["is_member"] = True - return Response(data, status=status.HTTP_201_CREATED) + project = self.get_queryset().filter(pk=serializer.data["id"]).first() + serializer = ProjectListSerializer(project) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST, @@ -302,12 +284,6 @@ def create(self, request, slug): {"name": "The project name is already taken"}, status=status.HTTP_410_GONE, ) - else: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_410_GONE, - ) except Workspace.DoesNotExist as e: return Response( {"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND @@ -317,12 +293,6 @@ def create(self, request, slug): {"identifier": "The project identifier is already taken"}, status=status.HTTP_410_GONE, ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) def partial_update(self, request, slug, pk=None): try: @@ -353,6 +323,8 @@ def partial_update(self, request, slug, pk=None): color="#ff7700", ) + project = self.get_queryset().filter(pk=serializer.data["id"]).first() + serializer = ProjectListSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -362,7 +334,7 @@ def partial_update(self, request, slug, pk=None): {"name": "The project name is already taken"}, status=status.HTTP_410_GONE, ) - except Project.DoesNotExist or Workspace.DoesNotExist as e: + except (Project.DoesNotExist, Workspace.DoesNotExist): return Response( {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND ) @@ -371,12 +343,6 @@ def partial_update(self, request, slug, pk=None): {"identifier": "The project identifier is already taken"}, status=status.HTTP_410_GONE, ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) class InviteProjectEndpoint(BaseAPIView): @@ -385,80 +351,62 @@ class InviteProjectEndpoint(BaseAPIView): ] def post(self, request, slug, project_id): - try: - email = request.data.get("email", False) - role = request.data.get("role", False) - - # Check if email is provided - if not email: - return Response( - {"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST - ) + email = request.data.get("email", False) + role = request.data.get("role", False) - validate_email(email) - # Check if user is already a member of workspace - if ProjectMember.objects.filter( - project_id=project_id, - member__email=email, - member__is_bot=False, - ).exists(): - return Response( - {"error": "User is already member of workspace"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - user = User.objects.filter(email=email).first() + # Check if email is provided + if not email: + return Response( + {"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST + ) - if user is None: - token = jwt.encode( - {"email": email, "timestamp": datetime.now().timestamp()}, - settings.SECRET_KEY, - algorithm="HS256", - ) - project_invitation_obj = ProjectMemberInvite.objects.create( - email=email.strip().lower(), - project_id=project_id, - token=token, - role=role, - ) - domain = settings.WEB_URL - project_invitation.delay(email, project_id, token, domain) + validate_email(email) + # Check if user is already a member of workspace + if ProjectMember.objects.filter( + project_id=project_id, + member__email=email, + member__is_bot=False, + ).exists(): + return Response( + {"error": "User is already member of workspace"}, + status=status.HTTP_400_BAD_REQUEST, + ) - return Response( - { - "message": "Email sent successfully", - "id": project_invitation_obj.id, - }, - status=status.HTTP_200_OK, - ) + user = User.objects.filter(email=email).first() - project_member = ProjectMember.objects.create( - member=user, project_id=project_id, role=role + if user is None: + token = jwt.encode( + {"email": email, "timestamp": datetime.now().timestamp()}, + settings.SECRET_KEY, + algorithm="HS256", ) - - return Response( - ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK + project_invitation_obj = ProjectMemberInvite.objects.create( + email=email.strip().lower(), + project_id=project_id, + token=token, + role=role, ) + domain = settings.WEB_URL + project_invitation.delay(email, project_id, token, domain) - except ValidationError: return Response( { - "error": "Invalid email address provided a valid email address is required to send the invite" + "message": "Email sent successfully", + "id": project_invitation_obj.id, }, - status=status.HTTP_400_BAD_REQUEST, - ) - except (Workspace.DoesNotExist, Project.DoesNotExist) as e: - return Response( - {"error": "Workspace or Project does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + status=status.HTTP_200_OK, ) + project_member = ProjectMember.objects.create( + member=user, project_id=project_id, role=role + ) + + _ = IssueProperty.objects.create(user=user, project_id=project_id) + + return Response( + ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK + ) + class UserProjectInvitationsViewset(BaseViewSet): serializer_class = ProjectMemberInviteSerializer @@ -473,41 +421,46 @@ def get_queryset(self): ) def create(self, request): - try: - invitations = request.data.get("invitations") - project_invitations = ProjectMemberInvite.objects.filter( - pk__in=invitations, accepted=True - ) - ProjectMember.objects.bulk_create( - [ - ProjectMember( - project=invitation.project, - workspace=invitation.project.workspace, - member=request.user, - role=invitation.role, - created_by=request.user, - ) - for invitation in project_invitations - ] - ) + invitations = request.data.get("invitations") + project_invitations = ProjectMemberInvite.objects.filter( + pk__in=invitations, accepted=True + ) + ProjectMember.objects.bulk_create( + [ + ProjectMember( + project=invitation.project, + workspace=invitation.project.workspace, + member=request.user, + role=invitation.role, + created_by=request.user, + ) + for invitation in project_invitations + ] + ) - # Delete joined project invites - project_invitations.delete() + IssueProperty.objects.bulk_create( + [ + ProjectMember( + project=invitation.project, + workspace=invitation.project.workspace, + user=request.user, + created_by=request.user, + ) + for invitation in project_invitations + ] + ) - return Response(status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + # Delete joined project invites + project_invitations.delete() + + return Response(status=status.HTTP_204_NO_CONTENT) class ProjectMemberViewSet(BaseViewSet): serializer_class = ProjectMemberAdminSerializer model = ProjectMember permission_classes = [ - ProjectBasePermission, + ProjectMemberPermission, ] search_fields = [ @@ -527,189 +480,167 @@ def get_queryset(self): .select_related("workspace", "workspace__owner") ) - def partial_update(self, request, slug, project_id, pk): - try: - project_member = ProjectMember.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id - ) - if request.user.id == project_member.member_id: - return Response( - {"error": "You cannot update your own role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - # Check while updating user roles - requested_project_member = ProjectMember.objects.get( - project_id=project_id, workspace__slug=slug, member=request.user - ) - if ( - "role" in request.data - and int(request.data.get("role", project_member.role)) - > requested_project_member.role - ): - return Response( - { - "error": "You cannot update a role that is higher than your own role" - }, - status=status.HTTP_400_BAD_REQUEST, - ) + def create(self, request, slug, project_id): + members = request.data.get("members", []) - serializer = ProjectMemberSerializer( - project_member, data=request.data, partial=True - ) + # get the project + project = Project.objects.get(pk=project_id, workspace__slug=slug) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except ProjectMember.DoesNotExist: - return Response( - {"error": "Project Member does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) + if not len(members): return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Atleast one member is required"}, status=status.HTTP_400_BAD_REQUEST, ) + bulk_project_members = [] + bulk_issue_props = [] - def destroy(self, request, slug, project_id, pk): - try: - project_member = ProjectMember.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk + project_members = ( + ProjectMember.objects.filter( + workspace__slug=slug, + member_id__in=[member.get("member_id") for member in members], ) - # check requesting user role - requesting_project_member = ProjectMember.objects.get( - workspace__slug=slug, member=request.user, project_id=project_id + .values("member_id", "sort_order") + .order_by("sort_order") + ) + + for member in members: + sort_order = [ + project_member.get("sort_order") + for project_member in project_members + if str(project_member.get("member_id")) == str(member.get("member_id")) + ] + bulk_project_members.append( + ProjectMember( + member_id=member.get("member_id"), + role=member.get("role", 10), + project_id=project_id, + workspace_id=project.workspace_id, + sort_order=sort_order[0] - 10000 if len(sort_order) else 65535, + ) ) - if requesting_project_member.role < project_member.role: - return Response( - { - "error": "You cannot remove a user having role higher than yourself" - }, - status=status.HTTP_400_BAD_REQUEST, + bulk_issue_props.append( + IssueProperty( + user_id=member.get("member_id"), + project_id=project_id, + workspace_id=project.workspace_id, ) - - # Remove all favorites - ProjectFavorite.objects.filter( - workspace__slug=slug, project_id=project_id, user=project_member.member - ).delete() - CycleFavorite.objects.filter( - workspace__slug=slug, project_id=project_id, user=project_member.member - ).delete() - ModuleFavorite.objects.filter( - workspace__slug=slug, project_id=project_id, user=project_member.member - ).delete() - PageFavorite.objects.filter( - workspace__slug=slug, project_id=project_id, user=project_member.member - ).delete() - IssueViewFavorite.objects.filter( - workspace__slug=slug, project_id=project_id, user=project_member.member - ).delete() - # Also remove issue from issue assigned - IssueAssignee.objects.filter( - workspace__slug=slug, - project_id=project_id, - assignee=project_member.member, - ).delete() - - # Remove if module member - ModuleMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - member=project_member.member, - ).delete() - # Delete owned Pages - Page.objects.filter( - workspace__slug=slug, - project_id=project_id, - owned_by=project_member.member, - ).delete() - project_member.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except ProjectMember.DoesNotExist: - return Response( - {"error": "Project Member does not exist"}, status=status.HTTP_400 ) - except Exception as e: - capture_exception(e) - return Response({"error": "Something went wrong please try again later"}) - -class AddMemberToProjectEndpoint(BaseAPIView): - permission_classes = [ - ProjectBasePermission, - ] + project_members = ProjectMember.objects.bulk_create( + bulk_project_members, + batch_size=10, + ignore_conflicts=True, + ) - def post(self, request, slug, project_id): - try: - members = request.data.get("members", []) + _ = IssueProperty.objects.bulk_create( + bulk_issue_props, batch_size=10, ignore_conflicts=True + ) - # get the project - project = Project.objects.get(pk=project_id, workspace__slug=slug) + serializer = ProjectMemberSerializer(project_members, many=True) - if not len(members): - return Response( - {"error": "Atleast one member is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - bulk_project_members = [] + return Response(serializer.data, status=status.HTTP_201_CREATED) - project_members = ( - ProjectMember.objects.filter( - workspace__slug=slug, - member_id__in=[member.get("member_id") for member in members], - ) - .values("member_id", "sort_order") - .order_by("sort_order") - ) + def list(self, request, slug, project_id): + project_member = ProjectMember.objects.get( + member=request.user, workspace__slug=slug, project_id=project_id + ) - for member in members: - sort_order = [ - project_member.get("sort_order") - for project_member in project_members - if str(project_member.get("member_id")) - == str(member.get("member_id")) - ] - bulk_project_members.append( - ProjectMember( - member_id=member.get("member_id"), - role=member.get("role", 10), - project_id=project_id, - workspace_id=project.workspace_id, - sort_order=sort_order[0] - 10000 if len(sort_order) else 65535, - ) - ) - - project_members = ProjectMember.objects.bulk_create( - bulk_project_members, - batch_size=10, - ignore_conflicts=True, - ) + project_members = ProjectMember.objects.filter( + project_id=project_id, + workspace__slug=slug, + member__is_bot=False, + ).select_related("project", "member", "workspace") + if project_member.role > 10: + serializer = ProjectMemberAdminSerializer(project_members, many=True) + else: serializer = ProjectMemberSerializer(project_members, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.data, status=status.HTTP_201_CREATED) - except KeyError: - return Response( - {"error": "Incorrect data sent"}, status=status.HTTP_400_BAD_REQUEST - ) - except Project.DoesNotExist: + def partial_update(self, request, slug, project_id, pk): + project_member = ProjectMember.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id + ) + if request.user.id == project_member.member_id: return Response( - {"error": "Project does not exist"}, status=status.HTTP_400_BAD_REQUEST + {"error": "You cannot update your own role"}, + status=status.HTTP_400_BAD_REQUEST, ) - except IntegrityError: + # Check while updating user roles + requested_project_member = ProjectMember.objects.get( + project_id=project_id, workspace__slug=slug, member=request.user + ) + if ( + "role" in request.data + and int(request.data.get("role", project_member.role)) + > requested_project_member.role + ): return Response( - {"error": "User not member of the workspace"}, + {"error": "You cannot update a role that is higher than your own role"}, status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) + + serializer = ProjectMemberSerializer( + project_member, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, pk): + project_member = ProjectMember.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + # check requesting user role + requesting_project_member = ProjectMember.objects.get( + workspace__slug=slug, member=request.user, project_id=project_id + ) + if requesting_project_member.role < project_member.role: return Response( - {"error": "Something went wrong please try again later"}, + {"error": "You cannot remove a user having role higher than yourself"}, status=status.HTTP_400_BAD_REQUEST, ) + # Remove all favorites + ProjectFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + CycleFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + ModuleFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + PageFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + IssueViewFavorite.objects.filter( + workspace__slug=slug, project_id=project_id, user=project_member.member + ).delete() + # Also remove issue from issue assigned + IssueAssignee.objects.filter( + workspace__slug=slug, + project_id=project_id, + assignee=project_member.member, + ).delete() + + # Remove if module member + ModuleMember.objects.filter( + workspace__slug=slug, + project_id=project_id, + member=project_member.member, + ).delete() + # Delete owned Pages + Page.objects.filter( + workspace__slug=slug, + project_id=project_id, + owned_by=project_member.member, + ).delete() + project_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + class AddTeamToProjectEndpoint(BaseAPIView): permission_classes = [ @@ -717,53 +648,48 @@ class AddTeamToProjectEndpoint(BaseAPIView): ] def post(self, request, slug, project_id): - try: - team_members = TeamMember.objects.filter( - workspace__slug=slug, team__in=request.data.get("teams", []) - ).values_list("member", flat=True) + team_members = TeamMember.objects.filter( + workspace__slug=slug, team__in=request.data.get("teams", []) + ).values_list("member", flat=True) - if len(team_members) == 0: - return Response( - {"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST - ) + if len(team_members) == 0: + return Response( + {"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST + ) - workspace = Workspace.objects.get(slug=slug) + workspace = Workspace.objects.get(slug=slug) - project_members = [] - for member in team_members: - project_members.append( - ProjectMember( - project_id=project_id, - member_id=member, - workspace=workspace, - created_by=request.user, - ) + project_members = [] + issue_props = [] + for member in team_members: + project_members.append( + ProjectMember( + project_id=project_id, + member_id=member, + workspace=workspace, + created_by=request.user, ) - - ProjectMember.objects.bulk_create( - project_members, batch_size=10, ignore_conflicts=True ) - - serializer = ProjectMemberSerializer(project_members, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "The team with the name already exists"}, - status=status.HTTP_410_GONE, + issue_props.append( + IssueProperty( + project_id=project_id, + user_id=member, + workspace=workspace, + created_by=request.user, ) - except Workspace.DoesNotExist: - return Response( - {"error": "The requested workspace could not be found"}, - status=status.HTTP_404_NOT_FOUND, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, ) + ProjectMember.objects.bulk_create( + project_members, batch_size=10, ignore_conflicts=True + ) + + _ = IssueProperty.objects.bulk_create( + issue_props, batch_size=10, ignore_conflicts=True + ) + + serializer = ProjectMemberSerializer(project_members, many=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + class ProjectMemberInvitationsViewset(BaseViewSet): serializer_class = ProjectMemberInviteSerializer @@ -811,166 +737,124 @@ class ProjectIdentifierEndpoint(BaseAPIView): ] def get(self, request, slug): - try: - name = request.GET.get("name", "").strip().upper() - - if name == "": - return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST - ) - - exists = ProjectIdentifier.objects.filter( - name=name, workspace__slug=slug - ).values("id", "name", "project") + name = request.GET.get("name", "").strip().upper() + if name == "": return Response( - {"exists": len(exists), "identifiers": exists}, - status=status.HTTP_200_OK, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST ) - def delete(self, request, slug): - try: - name = request.data.get("name", "").strip().upper() + exists = ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug + ).values("id", "name", "project") - if name == "": - return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST - ) - - if Project.objects.filter(identifier=name, workspace__slug=slug).exists(): - return Response( - {"error": "Cannot delete an identifier of an existing project"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response( + {"exists": len(exists), "identifiers": exists}, + status=status.HTTP_200_OK, + ) - ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete() + def delete(self, request, slug): + name = request.data.get("name", "").strip().upper() + if name == "": return Response( - status=status.HTTP_204_NO_CONTENT, + {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST ) - except Exception as e: - capture_exception(e) + + if Project.objects.filter(identifier=name, workspace__slug=slug).exists(): return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Cannot delete an identifier of an existing project"}, status=status.HTTP_400_BAD_REQUEST, ) + ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete() + + return Response( + status=status.HTTP_204_NO_CONTENT, + ) + class ProjectJoinEndpoint(BaseAPIView): def post(self, request, slug): - try: - project_ids = request.data.get("project_ids", []) + project_ids = request.data.get("project_ids", []) - # Get the workspace user role - workspace_member = WorkspaceMember.objects.get( - member=request.user, workspace__slug=slug - ) + # Get the workspace user role + workspace_member = WorkspaceMember.objects.get( + member=request.user, workspace__slug=slug + ) - workspace_role = workspace_member.role - workspace = workspace_member.workspace - - ProjectMember.objects.bulk_create( - [ - ProjectMember( - project_id=project_id, - member=request.user, - role=20 - if workspace_role >= 15 - else (15 if workspace_role == 10 else workspace_role), - workspace=workspace, - created_by=request.user, - ) - for project_id in project_ids - ], - ignore_conflicts=True, - ) + workspace_role = workspace_member.role + workspace = workspace_member.workspace - return Response( - {"message": "Projects joined successfully"}, - status=status.HTTP_201_CREATED, - ) - except WorkspaceMember.DoesNotExist: - return Response( - {"error": "User is not a member of workspace"}, - status=status.HTTP_403_FORBIDDEN, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + ProjectMember.objects.bulk_create( + [ + ProjectMember( + project_id=project_id, + member=request.user, + role=20 + if workspace_role >= 15 + else (15 if workspace_role == 10 else workspace_role), + workspace=workspace, + created_by=request.user, + ) + for project_id in project_ids + ], + ignore_conflicts=True, + ) + + IssueProperty.objects.bulk_create( + [ + IssueProperty( + project_id=project_id, + user=request.user, + workspace=workspace, + created_by=request.user, + ) + for project_id in project_ids + ], + ignore_conflicts=True, + ) + + return Response( + {"message": "Projects joined successfully"}, + status=status.HTTP_201_CREATED, + ) class ProjectUserViewsEndpoint(BaseAPIView): def post(self, request, slug, project_id): - try: - project = Project.objects.get(pk=project_id, workspace__slug=slug) + project = Project.objects.get(pk=project_id, workspace__slug=slug) - project_member = ProjectMember.objects.filter( - member=request.user, project=project - ).first() + project_member = ProjectMember.objects.filter( + member=request.user, project=project + ).first() - if project_member is None: - return Response( - {"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN - ) + if project_member is None: + return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) - view_props = project_member.view_props - default_props = project_member.default_props - preferences = project_member.preferences - sort_order = project_member.sort_order + view_props = project_member.view_props + default_props = project_member.default_props + preferences = project_member.preferences + sort_order = project_member.sort_order - project_member.view_props = request.data.get("view_props", view_props) - project_member.default_props = request.data.get( - "default_props", default_props - ) - project_member.preferences = request.data.get("preferences", preferences) - project_member.sort_order = request.data.get("sort_order", sort_order) + project_member.view_props = request.data.get("view_props", view_props) + project_member.default_props = request.data.get("default_props", default_props) + project_member.preferences = request.data.get("preferences", preferences) + project_member.sort_order = request.data.get("sort_order", sort_order) - project_member.save() + project_member.save() - return Response(status=status.HTTP_200_OK) - - except Project.DoesNotExist: - return Response( - {"error": "The requested resource does not exists"}, - status=status.HTTP_404_NOT_FOUND, - ) - except Exception as e: - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(status=status.HTTP_204_NO_CONTENT) class ProjectMemberUserEndpoint(BaseAPIView): def get(self, request, slug, project_id): - try: - project_member = ProjectMember.objects.get( - project_id=project_id, workspace__slug=slug, member=request.user - ) - serializer = ProjectMemberSerializer(project_member) - - return Response(serializer.data, status=status.HTTP_200_OK) + project_member = ProjectMember.objects.get( + project_id=project_id, workspace__slug=slug, member=request.user + ) + serializer = ProjectMemberSerializer(project_member) - except ProjectMember.DoesNotExist: - return Response( - {"error": "User not a member of the project"}, - status=status.HTTP_403_FORBIDDEN, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(serializer.data, status=status.HTTP_200_OK) class ProjectFavoritesViewSet(BaseViewSet): @@ -993,50 +877,18 @@ def perform_create(self, serializer): serializer.save(user=self.request.user) def create(self, request, slug): - try: - serializer = ProjectFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except IntegrityError as e: - print(str(e)) - if "already exists" in str(e): - return Response( - {"error": "The project is already added to favorites"}, - status=status.HTTP_410_GONE, - ) - else: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_410_GONE, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = ProjectFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, slug, project_id): - try: - project_favorite = ProjectFavorite.objects.get( - project=project_id, user=request.user, workspace__slug=slug - ) - project_favorite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except ProjectFavorite.DoesNotExist: - return Response( - {"error": "Project is not in favorites"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + project_favorite = ProjectFavorite.objects.get( + project=project_id, user=request.user, workspace__slug=slug + ) + project_favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class ProjectDeployBoardViewSet(BaseViewSet): @@ -1058,121 +910,138 @@ def get_queryset(self): ) def create(self, request, slug, project_id): - try: - comments = request.data.get("comments", False) - reactions = request.data.get("reactions", False) - inbox = request.data.get("inbox", None) - votes = request.data.get("votes", False) - views = request.data.get( - "views", - { - "list": True, - "kanban": True, - "calendar": True, - "gantt": True, - "spreadsheet": True, - }, - ) + comments = request.data.get("comments", False) + reactions = request.data.get("reactions", False) + inbox = request.data.get("inbox", None) + votes = request.data.get("votes", False) + views = request.data.get( + "views", + { + "list": True, + "kanban": True, + "calendar": True, + "gantt": True, + "spreadsheet": True, + }, + ) - project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( - anchor=f"{slug}/{project_id}", - project_id=project_id, - ) - project_deploy_board.comments = comments - project_deploy_board.reactions = reactions - project_deploy_board.inbox = inbox - project_deploy_board.votes = votes - project_deploy_board.views = views + project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( + anchor=f"{slug}/{project_id}", + project_id=project_id, + ) + project_deploy_board.comments = comments + project_deploy_board.reactions = reactions + project_deploy_board.inbox = inbox + project_deploy_board.votes = votes + project_deploy_board.views = views - project_deploy_board.save() + project_deploy_board.save() - serializer = ProjectDeployBoardSerializer(project_deploy_board) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = ProjectDeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) -class ProjectMemberEndpoint(BaseAPIView): +class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): permission_classes = [ - ProjectEntityPermission, + AllowAny, ] def get(self, request, slug, project_id): - try: - project_members = ProjectMember.objects.filter( - project_id=project_id, - workspace__slug=slug, - member__is_bot=False, - ).select_related("project", "member") - serializer = ProjectMemberSerializer(project_members, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + serializer = ProjectDeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) -class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): +class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] - def get(self, request, slug, project_id): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - serializer = ProjectDeployBoardSerializer(project_deploy_board) - return Response(serializer.data, status=status.HTTP_200_OK) - except ProjectDeployBoard.DoesNotExist: - return Response( - {"error": "Project Deploy Board does not exists"}, - status=status.HTTP_404_NOT_FOUND, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + def get(self, request, slug): + projects = ( + Project.objects.filter(workspace__slug=slug) + .annotate( + is_public=Exists( + ProjectDeployBoard.objects.filter( + workspace__slug=slug, project_id=OuterRef("pk") + ) + ) ) + .filter(is_public=True) + ).values( + "id", + "identifier", + "name", + "description", + "emoji", + "icon_prop", + "cover_image", + ) + return Response(projects, status=status.HTTP_200_OK) -class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): - permission_classes = [AllowAny,] +class LeaveProjectEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] - def get(self, request, slug): - try: - projects = ( - Project.objects.filter(workspace__slug=slug) - .annotate( - is_public=Exists( - ProjectDeployBoard.objects.filter( - workspace__slug=slug, project_id=OuterRef("pk") - ) - ) - ) - .filter(is_public=True) - ).values( - "id", - "identifier", - "name", - "description", - "emoji", - "icon_prop", - "cover_image", - ) + def delete(self, request, slug, project_id): + project_member = ProjectMember.objects.get( + workspace__slug=slug, + member=request.user, + project_id=project_id, + ) - return Response(projects, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) + # Only Admin case + if ( + project_member.role == 20 + and ProjectMember.objects.filter( + workspace__slug=slug, + role=20, + project_id=project_id, + ).count() + == 1 + ): return Response( - {"error": "Something went wrong please try again later"}, + { + "error": "You cannot leave the project since you are the only admin of the project you should delete the project" + }, status=status.HTTP_400_BAD_REQUEST, ) + # Delete the member from workspace + project_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProjectPublicCoverImagesEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request): + files = [] + s3 = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + params = { + "Bucket": settings.AWS_S3_BUCKET_NAME, + "Prefix": "static/project-cover/", + } + + response = s3.list_objects_v2(**params) + # Extracting file keys from the response + if "Contents" in response: + for content in response["Contents"]: + if not content["Key"].endswith( + "/" + ): # This line ensures we're only getting files, not "sub-folders" + files.append( + f"https://{settings.AWS_S3_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" + ) + + return Response(files, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/release.py b/apiserver/plane/api/views/release.py deleted file mode 100644 index de827c896e3..00000000000 --- a/apiserver/plane/api/views/release.py +++ /dev/null @@ -1,21 +0,0 @@ -# Third party imports -from rest_framework.response import Response -from rest_framework import status -from sentry_sdk import capture_exception - -# Module imports -from .base import BaseAPIView -from plane.utils.integrations.github import get_release_notes - - -class ReleaseNotesEndpoint(BaseAPIView): - def get(self, request): - try: - release_notes = get_release_notes() - return Response(release_notes, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py index 0a8c5c5306d..ff743154357 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/api/views/search.py @@ -168,126 +168,107 @@ def filter_views(self, query, slug, project_id, workspace_search): ) def get(self, request, slug): - try: - query = request.query_params.get("search", False) - workspace_search = request.query_params.get("workspace_search", "false") - project_id = request.query_params.get("project_id", False) - - if not query: - return Response( - { - "results": { - "workspace": [], - "project": [], - "issue": [], - "cycle": [], - "module": [], - "issue_view": [], - "page": [], - } - }, - status=status.HTTP_200_OK, - ) + query = request.query_params.get("search", False) + workspace_search = request.query_params.get("workspace_search", "false") + project_id = request.query_params.get("project_id", False) - MODELS_MAPPER = { - "workspace": self.filter_workspaces, - "project": self.filter_projects, - "issue": self.filter_issues, - "cycle": self.filter_cycles, - "module": self.filter_modules, - "issue_view": self.filter_views, - "page": self.filter_pages, - } - - results = {} - - for model in MODELS_MAPPER.keys(): - func = MODELS_MAPPER.get(model, None) - results[model] = func(query, slug, project_id, workspace_search) - return Response({"results": results}, status=status.HTTP_200_OK) - - except Exception as e: - capture_exception(e) + if not query: return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + { + "results": { + "workspace": [], + "project": [], + "issue": [], + "cycle": [], + "module": [], + "issue_view": [], + "page": [], + } + }, + status=status.HTTP_200_OK, ) + MODELS_MAPPER = { + "workspace": self.filter_workspaces, + "project": self.filter_projects, + "issue": self.filter_issues, + "cycle": self.filter_cycles, + "module": self.filter_modules, + "issue_view": self.filter_views, + "page": self.filter_pages, + } + + results = {} + + for model in MODELS_MAPPER.keys(): + func = MODELS_MAPPER.get(model, None) + results[model] = func(query, slug, project_id, workspace_search) + return Response({"results": results}, status=status.HTTP_200_OK) + class IssueSearchEndpoint(BaseAPIView): def get(self, request, slug, project_id): - try: - query = request.query_params.get("search", False) - workspace_search = request.query_params.get("workspace_search", "false") - parent = request.query_params.get("parent", "false") - blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false") - cycle = request.query_params.get("cycle", "false") - module = request.query_params.get("module", "false") - sub_issue = request.query_params.get("sub_issue", "false") - - issue_id = request.query_params.get("issue_id", False) - - issues = Issue.issue_objects.filter( - workspace__slug=slug, - project__project_projectmember__member=self.request.user, - ) - - if workspace_search == "false": - issues = issues.filter(project_id=project_id) + query = request.query_params.get("search", False) + workspace_search = request.query_params.get("workspace_search", "false") + parent = request.query_params.get("parent", "false") + issue_relation = request.query_params.get("issue_relation", "false") + cycle = request.query_params.get("cycle", "false") + module = request.query_params.get("module", "false") + sub_issue = request.query_params.get("sub_issue", "false") - if query: - issues = search_issues(query, issues) + issue_id = request.query_params.get("issue_id", False) - if parent == "true" and issue_id: - issue = Issue.issue_objects.get(pk=issue_id) - issues = issues.filter( - ~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True - ).exclude( - pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list( - "parent_id", flat=True - ) - ) - if blocker_blocked_by == "true" and issue_id: - issue = Issue.issue_objects.get(pk=issue_id) - issues = issues.filter( - ~Q(pk=issue_id), - ~Q(blocked_issues__block=issue), - ~Q(blocker_issues__blocked_by=issue), - ) - if sub_issue == "true" and issue_id: - issue = Issue.issue_objects.get(pk=issue_id) - issues = issues.filter(~Q(pk=issue_id), parent__isnull=True) - if issue.parent: - issues = issues.filter(~Q(pk=issue.parent_id)) + issues = Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__member=self.request.user, + ) - if cycle == "true": - issues = issues.exclude(issue_cycle__isnull=False) + if workspace_search == "false": + issues = issues.filter(project_id=project_id) - if module == "true": - issues = issues.exclude(issue_module__isnull=False) + if query: + issues = search_issues(query, issues) - return Response( - issues.values( - "name", - "id", - "sequence_id", - "project__name", - "project__identifier", - "project_id", - "workspace__slug", - "state__name", - "state__group", - "state__color", - ), - status=status.HTTP_200_OK, + if parent == "true" and issue_id: + issue = Issue.issue_objects.get(pk=issue_id) + issues = issues.filter( + ~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True + ).exclude( + pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list( + "parent_id", flat=True + ) ) - except Issue.DoesNotExist: - return Response( - {"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - print(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + if issue_relation == "true" and issue_id: + issue = Issue.issue_objects.get(pk=issue_id) + issues = issues.filter( + ~Q(pk=issue_id), + ~Q(issue_related__issue=issue), + ~Q(issue_relation__related_issue=issue), ) + if sub_issue == "true" and issue_id: + issue = Issue.issue_objects.get(pk=issue_id) + issues = issues.filter(~Q(pk=issue_id), parent__isnull=True) + if issue.parent: + issues = issues.filter(~Q(pk=issue.parent_id)) + + if cycle == "true": + issues = issues.exclude(issue_cycle__isnull=False) + + if module == "true": + issues = issues.exclude(issue_module__isnull=False) + + return Response( + issues.values( + "name", + "id", + "sequence_id", + "project__name", + "project__identifier", + "project_id", + "workspace__slug", + "state__name", + "state__group", + "state__color", + ), + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 4fe0c826014..063abf0e312 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -2,7 +2,6 @@ from itertools import groupby # Django imports -from django.db import IntegrityError from django.db.models import Q # Third party imports @@ -41,67 +40,45 @@ def get_queryset(self): ) def create(self, request, slug, project_id): - try: - serializer = StateSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(project_id=project_id) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except IntegrityError: - return Response( - {"error": "State with the name already exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = StateSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def list(self, request, slug, project_id): - try: - 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) - except Exception as e: - capture_exception(e) + 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) + + def destroy(self, request, slug, project_id, pk): + state = State.objects.get( + ~Q(name="Triage"), + pk=pk, project_id=project_id, workspace__slug=slug, + ) + + if state.default: return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + {"error": "Default state cannot be deleted"}, status=False ) - def destroy(self, request, slug, project_id, pk): - try: - state = State.objects.get( - ~Q(name="Triage"), - pk=pk, project_id=project_id, workspace__slug=slug, + # 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" + }, + status=status.HTTP_400_BAD_REQUEST, ) - if state.default: - 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" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - state.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except State.DoesNotExist: - return Response({"error": "State does not exists"}, status=status.HTTP_404) + state.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/user.py b/apiserver/plane/api/views/user.py index 68958e5041c..2e40565b466 100644 --- a/apiserver/plane/api/views/user.py +++ b/apiserver/plane/api/views/user.py @@ -8,6 +8,8 @@ from plane.api.serializers import ( UserSerializer, IssueActivitySerializer, + UserMeSerializer, + UserMeSettingsSerializer, ) from plane.api.views.base import BaseViewSet, BaseAPIView @@ -17,7 +19,6 @@ WorkspaceMemberInvite, Issue, IssueActivity, - WorkspaceMember, ) from plane.utils.paginator import BasePaginator @@ -30,129 +31,43 @@ def get_object(self): return self.request.user def retrieve(self, request): - try: - workspace = Workspace.objects.get( - pk=request.user.last_workspace_id, workspace_member__member=request.user - ) - workspace_invites = WorkspaceMemberInvite.objects.filter( - email=request.user.email - ).count() - assigned_issues = Issue.issue_objects.filter( - assignees__in=[request.user] - ).count() + serialized_data = UserMeSerializer(request.user).data + return Response( + serialized_data, + status=status.HTTP_200_OK, + ) - serialized_data = UserSerializer(request.user).data - serialized_data["workspace"] = { - "last_workspace_id": request.user.last_workspace_id, - "last_workspace_slug": workspace.slug, - "fallback_workspace_id": request.user.last_workspace_id, - "fallback_workspace_slug": workspace.slug, - "invites": workspace_invites, - } - serialized_data.setdefault("issues", {})[ - "assigned_issues" - ] = assigned_issues - - return Response( - serialized_data, - status=status.HTTP_200_OK, - ) - except Workspace.DoesNotExist: - # This exception will be hit even when the `last_workspace_id` is None - - workspace_invites = WorkspaceMemberInvite.objects.filter( - email=request.user.email - ).count() - assigned_issues = Issue.issue_objects.filter( - assignees__in=[request.user] - ).count() - - fallback_workspace = ( - Workspace.objects.filter(workspace_member__member=request.user) - .order_by("created_at") - .first() - ) - - serialized_data = UserSerializer(request.user).data - - serialized_data["workspace"] = { - "last_workspace_id": None, - "last_workspace_slug": None, - "fallback_workspace_id": fallback_workspace.id - if fallback_workspace is not None - else None, - "fallback_workspace_slug": fallback_workspace.slug - if fallback_workspace is not None - else None, - "invites": workspace_invites, - } - serialized_data.setdefault("issues", {})[ - "assigned_issues" - ] = assigned_issues - - return Response( - serialized_data, - status=status.HTTP_200_OK, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + def retrieve_user_settings(self, request): + serialized_data = UserMeSettingsSerializer(request.user).data + return Response(serialized_data, status=status.HTTP_200_OK) class UpdateUserOnBoardedEndpoint(BaseAPIView): def patch(self, request): - try: - user = User.objects.get(pk=request.user.id) - user.is_onboarded = request.data.get("is_onboarded", False) - user.save() - return Response( - {"message": "Updated successfully"}, status=status.HTTP_200_OK - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + user = User.objects.get(pk=request.user.id) + user.is_onboarded = request.data.get("is_onboarded", False) + user.save() + return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK) class UpdateUserTourCompletedEndpoint(BaseAPIView): def patch(self, request): - try: - user = User.objects.get(pk=request.user.id) - user.is_tour_completed = request.data.get("is_tour_completed", False) - user.save() - return Response( - {"message": "Updated successfully"}, status=status.HTTP_200_OK - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + user = User.objects.get(pk=request.user.id) + user.is_tour_completed = request.data.get("is_tour_completed", False) + user.save() + return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK) class UserActivityEndpoint(BaseAPIView, BasePaginator): def get(self, request, slug): - try: - queryset = IssueActivity.objects.filter( - actor=request.user, workspace__slug=slug - ).select_related("actor", "workspace", "issue", "project") - - return self.paginate( - request=request, - queryset=queryset, - on_results=lambda issue_activities: IssueActivitySerializer( - issue_activities, many=True - ).data, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + queryset = IssueActivity.objects.filter( + actor=request.user, workspace__slug=slug + ).select_related("actor", "workspace", "issue", "project") + + return self.paginate( + request=request, + queryset=queryset, + on_results=lambda issue_activities: IssueActivitySerializer( + issue_activities, many=True + ).data, + ) diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py index 32ba24c8b46..f58f320b77f 100644 --- a/apiserver/plane/api/views/view.py +++ b/apiserver/plane/api/views/view.py @@ -1,5 +1,18 @@ # Django imports -from django.db import IntegrityError +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Case, + Value, + CharField, + When, + Exists, + Max, +) +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page from django.db.models import Prefetch, OuterRef, Exists # Third party imports @@ -10,18 +23,180 @@ # Module imports from . import BaseViewSet, BaseAPIView from plane.api.serializers import ( + GlobalViewSerializer, IssueViewSerializer, IssueLiteSerializer, IssueViewFavoriteSerializer, ) -from plane.api.permissions import ProjectEntityPermission +from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission from plane.db.models import ( + Workspace, + GlobalView, IssueView, Issue, IssueViewFavorite, IssueReaction, + IssueLink, + IssueAttachment, ) from plane.utils.issue_filters import issue_filters +from plane.utils.grouper import group_results + + +class GlobalViewViewSet(BaseViewSet): + serializer_class = GlobalViewSerializer + model = GlobalView + permission_classes = [ + WorkspaceEntityPermission, + ] + + def perform_create(self, serializer): + workspace = Workspace.objects.get(slug=self.kwargs.get("slug")) + serializer.save(workspace_id=workspace.id) + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("workspace") + .order_by(self.request.GET.get("order_by", "-created_at")) + .distinct() + ) + + +class GlobalViewIssuesViewSet(BaseViewSet): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get_queryset(self): + return ( + Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(workspace__slug=self.kwargs.get("slug")) + .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"), + ) + ) + ) + + @method_decorator(gzip_page) + def list(self, request, slug): + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + self.get_queryset() + .filter(**filters) + .filter(project__project_projectmember__member=self.request.user) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order if order_by_param == "priority" else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + issues = IssueLiteSerializer(issue_queryset, many=True).data + + ## Grouping the results + group_by = request.GET.get("group_by", False) + sub_group_by = request.GET.get("sub_group_by", False) + if sub_group_by and sub_group_by == group_by: + return Response( + {"error": "Group by and sub group by cannot be same"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if group_by: + grouped_results = group_results(issues, group_by, sub_group_by) + return Response( + grouped_results, + status=status.HTTP_200_OK, + ) + + return Response(issues, status=status.HTTP_200_OK) class IssueViewViewSet(BaseViewSet): @@ -55,51 +230,6 @@ def get_queryset(self): ) -class ViewIssuesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id, view_id): - try: - view = IssueView.objects.get(pk=view_id) - queries = view.query - - filters = issue_filters(request.query_params, "GET") - - issues = ( - Issue.issue_objects.filter( - **queries, project_id=project_id, workspace__slug=slug - ) - .filter(**filters) - .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"), - ) - ) - ) - - serializer = IssueLiteSerializer(issues, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except IssueView.DoesNotExist: - return Response( - {"error": "Issue View does not exist"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - class IssueViewFavoriteViewSet(BaseViewSet): serializer_class = IssueViewFavoriteSerializer model = IssueViewFavorite @@ -114,49 +244,18 @@ def get_queryset(self): ) def create(self, request, slug, project_id): - try: - serializer = IssueViewFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user, project_id=project_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "The view is already added to favorites"}, - status=status.HTTP_410_GONE, - ) - else: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = IssueViewFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user, project_id=project_id) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, slug, project_id, view_id): - try: - view_favourite = IssueViewFavorite.objects.get( - project=project_id, - user=request.user, - workspace__slug=slug, - view_id=view_id, - ) - view_favourite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except IssueViewFavorite.DoesNotExist: - return Response( - {"error": "View is not in favorites"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + view_favourite = IssueViewFavorite.objects.get( + project=project_id, + user=request.user, + workspace__slug=slug, + view_id=view_id, + ) + view_favourite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index b10fe3d42c8..c53fbf126bc 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -6,12 +6,10 @@ # Django imports from django.db import IntegrityError -from django.db.models import Prefetch from django.conf import settings from django.utils import timezone from django.core.exceptions import ValidationError from django.core.validators import validate_email -from django.contrib.sites.shortcuts import get_current_site from django.db.models import ( Prefetch, OuterRef, @@ -48,13 +46,13 @@ IssueActivitySerializer, IssueLiteSerializer, WorkspaceMemberAdminSerializer, + WorkspaceMemberMeSerializer, ) from plane.api.views.base import BaseAPIView from . import BaseViewSet from plane.db.models import ( User, Workspace, - WorkspaceMember, WorkspaceMemberInvite, Team, ProjectMember, @@ -116,7 +114,7 @@ def get_queryset(self): ) issue_count = ( - Issue.objects.filter(workspace=OuterRef("id")) + Issue.issue_objects.filter(workspace=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -164,23 +162,12 @@ def create(self, request): status=status.HTTP_400_BAD_REQUEST, ) - ## Handling unique integrity error for now - ## TODO: Extend this to handle other common errors which are not automatically handled by APIException except IntegrityError as e: if "already exists" in str(e): return Response( {"slug": "The workspace with the slug already exists"}, status=status.HTTP_410_GONE, ) - except Exception as e: - capture_exception(e) - return Response( - { - "error": "Something went wrong please try again later", - "identifier": None, - }, - status=status.HTTP_400_BAD_REQUEST, - ) class UserWorkSpacesEndpoint(BaseAPIView): @@ -192,70 +179,53 @@ class UserWorkSpacesEndpoint(BaseAPIView): ] def get(self, request): - try: - member_count = ( - WorkspaceMember.objects.filter( - workspace=OuterRef("id"), member__is_bot=False - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") + member_count = ( + WorkspaceMember.objects.filter( + workspace=OuterRef("id"), member__is_bot=False ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) - issue_count = ( - Issue.objects.filter(workspace=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) + issue_count = ( + Issue.issue_objects.filter(workspace=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) - workspace = ( - ( - Workspace.objects.prefetch_related( - Prefetch( - "workspace_member", queryset=WorkspaceMember.objects.all() - ) - ) - .filter( - workspace_member__member=request.user, - ) - .select_related("owner") + workspace = ( + ( + Workspace.objects.prefetch_related( + Prefetch("workspace_member", queryset=WorkspaceMember.objects.all()) + ) + .filter( + workspace_member__member=request.user, ) - .annotate(total_members=member_count) - .annotate(total_issues=issue_count) + .select_related("owner") ) + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + ) - serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) + return Response(serializer.data, status=status.HTTP_200_OK) class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): def get(self, request): - try: - slug = request.GET.get("slug", False) + slug = request.GET.get("slug", False) - if not slug or slug == "": - return Response( - {"error": "Workspace Slug is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.filter(slug=slug).exists() - return Response({"status": not workspace}, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) + if not slug or slug == "": return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Workspace Slug is required"}, status=status.HTTP_400_BAD_REQUEST, ) + workspace = Workspace.objects.filter(slug=slug).exists() + return Response({"status": not workspace}, status=status.HTTP_200_OK) + class InviteWorkspaceEndpoint(BaseAPIView): permission_classes = [ @@ -263,126 +233,113 @@ class InviteWorkspaceEndpoint(BaseAPIView): ] def post(self, request, slug): - try: - emails = request.data.get("emails", False) - # Check if email is provided - if not emails or not len(emails): - return Response( - {"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST - ) + emails = request.data.get("emails", False) + # Check if email is provided + if not emails or not len(emails): + return Response( + {"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST + ) - # check for role level - requesting_user = WorkspaceMember.objects.get( - workspace__slug=slug, member=request.user + # check for role level + requesting_user = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + if len( + [ + email + for email in emails + if int(email.get("role", 10)) > requesting_user.role + ] + ): + return Response( + {"error": "You cannot invite a user with higher role"}, + status=status.HTTP_400_BAD_REQUEST, ) - if len( - [ - email - for email in emails - if int(email.get("role", 10)) > requesting_user.role - ] - ): - return Response( - {"error": "You cannot invite a user with higher role"}, - status=status.HTTP_400_BAD_REQUEST, - ) - workspace = Workspace.objects.get(slug=slug) + workspace = Workspace.objects.get(slug=slug) + + # Check if user is already a member of workspace + workspace_members = WorkspaceMember.objects.filter( + workspace_id=workspace.id, + member__email__in=[email.get("email") for email in emails], + ).select_related("member", "workspace", "workspace__owner") - # Check if user is already a member of workspace - workspace_members = WorkspaceMember.objects.filter( - workspace_id=workspace.id, - member__email__in=[email.get("email") for email in emails], - ).select_related("member", "workspace", "workspace__owner") + if len(workspace_members): + return Response( + { + "error": "Some users are already member of workspace", + "workspace_users": WorkSpaceMemberSerializer( + workspace_members, many=True + ).data, + }, + status=status.HTTP_400_BAD_REQUEST, + ) - if len(workspace_members): + workspace_invitations = [] + for email in emails: + try: + validate_email(email.get("email")) + workspace_invitations.append( + WorkspaceMemberInvite( + email=email.get("email").strip().lower(), + workspace_id=workspace.id, + token=jwt.encode( + { + "email": email, + "timestamp": datetime.now().timestamp(), + }, + settings.SECRET_KEY, + algorithm="HS256", + ), + role=email.get("role", 10), + created_by=request.user, + ) + ) + except ValidationError: return Response( { - "error": "Some users are already member of workspace", - "workspace_users": WorkSpaceMemberSerializer( - workspace_members, many=True - ).data, + "error": f"Invalid email - {email} provided a valid email address is required to send the invite" }, status=status.HTTP_400_BAD_REQUEST, ) + WorkspaceMemberInvite.objects.bulk_create( + workspace_invitations, batch_size=10, ignore_conflicts=True + ) - workspace_invitations = [] - for email in emails: - try: - validate_email(email.get("email")) - workspace_invitations.append( - WorkspaceMemberInvite( - email=email.get("email").strip().lower(), - workspace_id=workspace.id, - token=jwt.encode( - { - "email": email, - "timestamp": datetime.now().timestamp(), - }, - settings.SECRET_KEY, - algorithm="HS256", - ), - role=email.get("role", 10), - created_by=request.user, - ) - ) - except ValidationError: - return Response( - { - "error": f"Invalid email - {email} provided a valid email address is required to send the invite" - }, - status=status.HTTP_400_BAD_REQUEST, + workspace_invitations = WorkspaceMemberInvite.objects.filter( + email__in=[email.get("email") for email in emails] + ).select_related("workspace") + + # create the user if signup is disabled + if settings.DOCKERIZED and not settings.ENABLE_SIGNUP: + _ = User.objects.bulk_create( + [ + User( + username=str(uuid4().hex), + email=invitation.email, + password=make_password(uuid4().hex), + is_password_autoset=True, ) - WorkspaceMemberInvite.objects.bulk_create( - workspace_invitations, batch_size=10, ignore_conflicts=True + for invitation in workspace_invitations + ], + batch_size=100, ) - workspace_invitations = WorkspaceMemberInvite.objects.filter( - email__in=[email.get("email") for email in emails] - ).select_related("workspace") - - # create the user if signup is disabled - if settings.DOCKERIZED and not settings.ENABLE_SIGNUP: - _ = User.objects.bulk_create( - [ - User( - username=str(uuid4().hex), - email=invitation.email, - password=make_password(uuid4().hex), - is_password_autoset=True, - ) - for invitation in workspace_invitations - ], - batch_size=100, - ) - - for invitation in workspace_invitations: - workspace_invitation.delay( - invitation.email, - workspace.id, - invitation.token, - settings.WEB_URL, - request.user.email, - ) - - return Response( - { - "message": "Emails sent successfully", - }, - status=status.HTTP_200_OK, + for invitation in workspace_invitations: + workspace_invitation.delay( + invitation.email, + workspace.id, + invitation.token, + settings.WEB_URL, + request.user.email, ) - except Workspace.DoesNotExist: - return Response( - {"error": "Workspace does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response( + { + "message": "Emails sent successfully", + }, + status=status.HTTP_200_OK, + ) class JoinWorkspaceEndpoint(BaseAPIView): @@ -391,68 +348,55 @@ class JoinWorkspaceEndpoint(BaseAPIView): ] def post(self, request, slug, pk): - try: - workspace_invite = WorkspaceMemberInvite.objects.get( - pk=pk, workspace__slug=slug - ) - - email = request.data.get("email", "") + workspace_invite = WorkspaceMemberInvite.objects.get( + pk=pk, workspace__slug=slug + ) - if email == "" or workspace_invite.email != email: - return Response( - {"error": "You do not have permission to join the workspace"}, - status=status.HTTP_403_FORBIDDEN, - ) + email = request.data.get("email", "") - if workspace_invite.responded_at is None: - workspace_invite.accepted = request.data.get("accepted", False) - workspace_invite.responded_at = timezone.now() - workspace_invite.save() + if email == "" or workspace_invite.email != email: + return Response( + {"error": "You do not have permission to join the workspace"}, + status=status.HTTP_403_FORBIDDEN, + ) - if workspace_invite.accepted: - # Check if the user created account after invitation - user = User.objects.filter(email=email).first() + if workspace_invite.responded_at is None: + workspace_invite.accepted = request.data.get("accepted", False) + workspace_invite.responded_at = timezone.now() + workspace_invite.save() - # If the user is present then create the workspace member - if user is not None: - WorkspaceMember.objects.create( - workspace=workspace_invite.workspace, - member=user, - role=workspace_invite.role, - ) + if workspace_invite.accepted: + # Check if the user created account after invitation + user = User.objects.filter(email=email).first() - user.last_workspace_id = workspace_invite.workspace.id - user.save() + # If the user is present then create the workspace member + if user is not None: + WorkspaceMember.objects.create( + workspace=workspace_invite.workspace, + member=user, + role=workspace_invite.role, + ) - # Delete the invitation - workspace_invite.delete() + user.last_workspace_id = workspace_invite.workspace.id + user.save() - return Response( - {"message": "Workspace Invitation Accepted"}, - status=status.HTTP_200_OK, - ) + # Delete the invitation + workspace_invite.delete() return Response( - {"message": "Workspace Invitation was not accepted"}, + {"message": "Workspace Invitation Accepted"}, status=status.HTTP_200_OK, ) return Response( - {"error": "You have already responded to the invitation request"}, - status=status.HTTP_400_BAD_REQUEST, + {"message": "Workspace Invitation was not accepted"}, + status=status.HTTP_200_OK, ) - except WorkspaceMemberInvite.DoesNotExist: - return Response( - {"error": "The invitation either got expired or could not be found"}, - status=status.HTTP_404_NOT_FOUND, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response( + {"error": "You have already responded to the invitation request"}, + status=status.HTTP_400_BAD_REQUEST, + ) class WorkspaceInvitationsViewset(BaseViewSet): @@ -472,28 +416,16 @@ def get_queryset(self): ) def destroy(self, request, slug, pk): - try: - workspace_member_invite = WorkspaceMemberInvite.objects.get( - pk=pk, workspace__slug=slug - ) - # delete the user if signup is disabled - if settings.DOCKERIZED and not settings.ENABLE_SIGNUP: - user = User.objects.filter(email=workspace_member_invite.email).first() - if user is not None: - user.delete() - workspace_member_invite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except WorkspaceMemberInvite.DoesNotExist: - return Response( - {"error": "Workspace member invite does not exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + workspace_member_invite = WorkspaceMemberInvite.objects.get( + pk=pk, workspace__slug=slug + ) + # delete the user if signup is disabled + if settings.DOCKERIZED and not settings.ENABLE_SIGNUP: + user = User.objects.filter(email=workspace_member_invite.email).first() + if user is not None: + user.delete() + workspace_member_invite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class UserWorkspaceInvitationsEndpoint(BaseViewSet): @@ -510,35 +442,26 @@ def get_queryset(self): ) def create(self, request): - try: - invitations = request.data.get("invitations") - workspace_invitations = WorkspaceMemberInvite.objects.filter( - pk__in=invitations - ) + invitations = request.data.get("invitations") + workspace_invitations = WorkspaceMemberInvite.objects.filter(pk__in=invitations) - WorkspaceMember.objects.bulk_create( - [ - WorkspaceMember( - workspace=invitation.workspace, - member=request.user, - role=invitation.role, - created_by=request.user, - ) - for invitation in workspace_invitations - ], - ignore_conflicts=True, - ) + WorkspaceMember.objects.bulk_create( + [ + WorkspaceMember( + workspace=invitation.workspace, + member=request.user, + role=invitation.role, + created_by=request.user, + ) + for invitation in workspace_invitations + ], + ignore_conflicts=True, + ) - # Delete joined workspace invites - workspace_invitations.delete() + # Delete joined workspace invites + workspace_invitations.delete() - return Response(status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(status=status.HTTP_204_NO_CONTENT) class WorkSpaceMemberViewSet(BaseViewSet): @@ -546,7 +469,7 @@ class WorkSpaceMemberViewSet(BaseViewSet): model = WorkspaceMember permission_classes = [ - WorkSpaceAdminPermission, + WorkspaceEntityPermission, ] search_fields = [ @@ -563,131 +486,124 @@ def get_queryset(self): .select_related("member") ) - def partial_update(self, request, slug, pk): - try: - workspace_member = WorkspaceMember.objects.get(pk=pk, workspace__slug=slug) - if request.user.id == workspace_member.member_id: - return Response( - {"error": "You cannot update your own role"}, - status=status.HTTP_400_BAD_REQUEST, - ) + def list(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + member=request.user, workspace__slug=slug + ) - # Get the requested user role - requested_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, member=request.user - ) - # Check if role is being updated - # One cannot update role higher than his own role - if ( - "role" in request.data - and int(request.data.get("role", workspace_member.role)) - > requested_workspace_member.role - ): - return Response( - { - "error": "You cannot update a role that is higher than your own role" - }, - status=status.HTTP_400_BAD_REQUEST, - ) + workspace_members = WorkspaceMember.objects.filter( + workspace__slug=slug, + member__is_bot=False, + ).select_related("workspace", "member") + if workspace_member.role > 10: + serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True) + else: serializer = WorkSpaceMemberSerializer( - workspace_member, data=request.data, partial=True + workspace_members, + many=True, ) + return Response(serializer.data, status=status.HTTP_200_OK) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except WorkspaceMember.DoesNotExist: + def partial_update(self, request, slug, pk): + workspace_member = WorkspaceMember.objects.get(pk=pk, workspace__slug=slug) + if request.user.id == workspace_member.member_id: return Response( - {"error": "Workspace Member does not exist"}, + {"error": "You cannot update your own role"}, status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) + + # Get the requested user role + requested_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + # Check if role is being updated + # One cannot update role higher than his own role + if ( + "role" in request.data + and int(request.data.get("role", workspace_member.role)) + > requested_workspace_member.role + ): return Response( - {"error": "Something went wrong please try again later"}, + {"error": "You cannot update a role that is higher than your own role"}, status=status.HTTP_400_BAD_REQUEST, ) - def destroy(self, request, slug, pk): - try: - # Check the user role who is deleting the user - workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, pk=pk) + serializer = WorkSpaceMemberSerializer( + workspace_member, data=request.data, partial=True + ) - # check requesting user role - requesting_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, member=request.user - ) - if requesting_workspace_member.role < workspace_member.role: - return Response( - {"error": "You cannot remove a user having role higher than you"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - # Check for the only member in the workspace - if ( - workspace_member.role == 20 - and WorkspaceMember.objects.filter( - workspace__slug=slug, - role=20, - member__is_bot=False, - ).count() - == 1 - ): - return Response( - {"error": "Cannot delete the only Admin for the workspace"}, - status=status.HTTP_400_BAD_REQUEST, - ) + def destroy(self, request, slug, pk): + # Check the user role who is deleting the user + workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, pk=pk) - # Delete the user also from all the projects - ProjectMember.objects.filter( - workspace__slug=slug, member=workspace_member.member - ).delete() - # Remove all favorites - ProjectFavorite.objects.filter( - workspace__slug=slug, user=workspace_member.member - ).delete() - CycleFavorite.objects.filter( - workspace__slug=slug, user=workspace_member.member - ).delete() - ModuleFavorite.objects.filter( - workspace__slug=slug, user=workspace_member.member - ).delete() - PageFavorite.objects.filter( - workspace__slug=slug, user=workspace_member.member - ).delete() - IssueViewFavorite.objects.filter( - workspace__slug=slug, user=workspace_member.member - ).delete() - # Also remove issue from issue assigned - IssueAssignee.objects.filter( - workspace__slug=slug, assignee=workspace_member.member - ).delete() - - # Remove if module member - ModuleMember.objects.filter( - workspace__slug=slug, member=workspace_member.member - ).delete() - # Delete owned Pages - Page.objects.filter( - workspace__slug=slug, owned_by=workspace_member.member - ).delete() - - workspace_member.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except WorkspaceMember.DoesNotExist: + # check requesting user role + requesting_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + if requesting_workspace_member.role < workspace_member.role: return Response( - {"error": "Workspace Member does not exists"}, + {"error": "You cannot remove a user having role higher than you"}, status=status.HTTP_400_BAD_REQUEST, ) - except Exception as e: - capture_exception(e) + + # Check for the only member in the workspace + if ( + workspace_member.role == 20 + and WorkspaceMember.objects.filter( + workspace__slug=slug, + role=20, + member__is_bot=False, + ).count() + == 1 + ): return Response( - {"error": "Something went wrong please try again later"}, + {"error": "Cannot delete the only Admin for the workspace"}, status=status.HTTP_400_BAD_REQUEST, ) + # Delete the user also from all the projects + ProjectMember.objects.filter( + workspace__slug=slug, member=workspace_member.member + ).delete() + # Remove all favorites + ProjectFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + CycleFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + ModuleFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + PageFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + IssueViewFavorite.objects.filter( + workspace__slug=slug, user=workspace_member.member + ).delete() + # Also remove issue from issue assigned + IssueAssignee.objects.filter( + workspace__slug=slug, assignee=workspace_member.member + ).delete() + + # Remove if module member + ModuleMember.objects.filter( + workspace__slug=slug, member=workspace_member.member + ).delete() + # Delete owned Pages + Page.objects.filter( + workspace__slug=slug, owned_by=workspace_member.member + ).delete() + + workspace_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + class TeamMemberViewSet(BaseViewSet): serializer_class = TeamSerializer @@ -711,51 +627,36 @@ def get_queryset(self): ) def create(self, request, slug): - try: - members = list( - WorkspaceMember.objects.filter( - workspace__slug=slug, member__id__in=request.data.get("members", []) - ) - .annotate(member_str_id=Cast("member", output_field=CharField())) - .distinct() - .values_list("member_str_id", flat=True) + members = list( + WorkspaceMember.objects.filter( + workspace__slug=slug, member__id__in=request.data.get("members", []) ) + .annotate(member_str_id=Cast("member", output_field=CharField())) + .distinct() + .values_list("member_str_id", flat=True) + ) - if len(members) != len(request.data.get("members", [])): - users = list(set(request.data.get("members", [])).difference(members)) - users = User.objects.filter(pk__in=users) - - serializer = UserLiteSerializer(users, many=True) - return Response( - { - "error": f"{len(users)} of the member(s) are not a part of the workspace", - "members": serializer.data, - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) + if len(members) != len(request.data.get("members", [])): + users = list(set(request.data.get("members", [])).difference(members)) + users = User.objects.filter(pk__in=users) - serializer = TeamSerializer( - data=request.data, context={"workspace": workspace} - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"error": "The team with the name already exists"}, - status=status.HTTP_410_GONE, - ) - except Exception as e: - capture_exception(e) + serializer = UserLiteSerializer(users, many=True) return Response( - {"error": "Something went wrong please try again later"}, + { + "error": f"{len(users)} of the member(s) are not a part of the workspace", + "members": serializer.data, + }, status=status.HTTP_400_BAD_REQUEST, ) + workspace = Workspace.objects.get(slug=slug) + + serializer = TeamSerializer(data=request.data, context={"workspace": workspace}) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class UserWorkspaceInvitationEndpoint(BaseViewSet): model = WorkspaceMemberInvite @@ -776,140 +677,93 @@ def get_queryset(self): class UserLastProjectWithWorkspaceEndpoint(BaseAPIView): def get(self, request): - try: - user = User.objects.get(pk=request.user.id) - - last_workspace_id = user.last_workspace_id - - if last_workspace_id is None: - return Response( - { - "project_details": [], - "workspace_details": {}, - }, - status=status.HTTP_200_OK, - ) - - workspace = Workspace.objects.get(pk=last_workspace_id) - workspace_serializer = WorkSpaceSerializer(workspace) + user = User.objects.get(pk=request.user.id) - project_member = ProjectMember.objects.filter( - workspace_id=last_workspace_id, member=request.user - ).select_related("workspace", "project", "member", "workspace__owner") - - project_member_serializer = ProjectMemberSerializer( - project_member, many=True - ) + last_workspace_id = user.last_workspace_id + if last_workspace_id is None: return Response( { - "workspace_details": workspace_serializer.data, - "project_details": project_member_serializer.data, + "project_details": [], + "workspace_details": {}, }, status=status.HTTP_200_OK, ) - except User.DoesNotExist: - return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + workspace = Workspace.objects.get(pk=last_workspace_id) + workspace_serializer = WorkSpaceSerializer(workspace) + + project_member = ProjectMember.objects.filter( + workspace_id=last_workspace_id, member=request.user + ).select_related("workspace", "project", "member", "workspace__owner") + + project_member_serializer = ProjectMemberSerializer(project_member, many=True) + + return Response( + { + "workspace_details": workspace_serializer.data, + "project_details": project_member_serializer.data, + }, + status=status.HTTP_200_OK, + ) class WorkspaceMemberUserEndpoint(BaseAPIView): def get(self, request, slug): - try: - workspace_member = WorkspaceMember.objects.get( - member=request.user, workspace__slug=slug - ) - serializer = WorkSpaceMemberSerializer(workspace_member) - return Response(serializer.data, status=status.HTTP_200_OK) - except (Workspace.DoesNotExist, WorkspaceMember.DoesNotExist): - return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + workspace_member = WorkspaceMember.objects.get( + member=request.user, workspace__slug=slug + ) + serializer = WorkspaceMemberMeSerializer(workspace_member) + return Response(serializer.data, status=status.HTTP_200_OK) class WorkspaceMemberUserViewsEndpoint(BaseAPIView): def post(self, request, slug): - try: - workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, member=request.user - ) - workspace_member.view_props = request.data.get("view_props", {}) - workspace_member.save() + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + workspace_member.view_props = request.data.get("view_props", {}) + workspace_member.save() - return Response(status=status.HTTP_200_OK) - except WorkspaceMember.DoesNotExist: - return Response( - {"error": "User not a member of workspace"}, - status=status.HTTP_403_FORBIDDEN, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(status=status.HTTP_204_NO_CONTENT) class UserActivityGraphEndpoint(BaseAPIView): def get(self, request, slug): - try: - issue_activities = ( - IssueActivity.objects.filter( - actor=request.user, - workspace__slug=slug, - created_at__date__gte=date.today() + relativedelta(months=-6), - ) - .annotate(created_date=Cast("created_at", DateField())) - .values("created_date") - .annotate(activity_count=Count("created_date")) - .order_by("created_date") + issue_activities = ( + IssueActivity.objects.filter( + actor=request.user, + workspace__slug=slug, + created_at__date__gte=date.today() + relativedelta(months=-6), ) + .annotate(created_date=Cast("created_at", DateField())) + .values("created_date") + .annotate(activity_count=Count("created_date")) + .order_by("created_date") + ) - return Response(issue_activities, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(issue_activities, status=status.HTTP_200_OK) class UserIssueCompletedGraphEndpoint(BaseAPIView): def get(self, request, slug): - try: - month = request.GET.get("month", 1) + month = request.GET.get("month", 1) - issues = ( - Issue.issue_objects.filter( - assignees__in=[request.user], - workspace__slug=slug, - completed_at__month=month, - completed_at__isnull=False, - ) - .annotate(completed_week=ExtractWeek("completed_at")) - .annotate(week=F("completed_week") % 4) - .values("week") - .annotate(completed_count=Count("completed_week")) - .order_by("week") - ) + issues = ( + Issue.issue_objects.filter( + assignees__in=[request.user], + workspace__slug=slug, + completed_at__month=month, + completed_at__isnull=False, + ) + .annotate(completed_week=ExtractWeek("completed_at")) + .annotate(week=F("completed_week") % 4) + .values("week") + .annotate(completed_count=Count("completed_week")) + .order_by("week") + ) - return Response(issues, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response(issues, status=status.HTTP_200_OK) class WeekInMonth(Func): @@ -919,108 +773,100 @@ class WeekInMonth(Func): class UserWorkspaceDashboardEndpoint(BaseAPIView): def get(self, request, slug): - try: - issue_activities = ( - IssueActivity.objects.filter( - actor=request.user, - workspace__slug=slug, - created_at__date__gte=date.today() + relativedelta(months=-3), - ) - .annotate(created_date=Cast("created_at", DateField())) - .values("created_date") - .annotate(activity_count=Count("created_date")) - .order_by("created_date") - ) - - month = request.GET.get("month", 1) - - completed_issues = ( - Issue.issue_objects.filter( - assignees__in=[request.user], - workspace__slug=slug, - completed_at__month=month, - completed_at__isnull=False, - ) - .annotate(day_of_month=ExtractDay("completed_at")) - .annotate(week_in_month=WeekInMonth(F("day_of_month"))) - .values("week_in_month") - .annotate(completed_count=Count("id")) - .order_by("week_in_month") + issue_activities = ( + IssueActivity.objects.filter( + actor=request.user, + workspace__slug=slug, + created_at__date__gte=date.today() + relativedelta(months=-3), ) + .annotate(created_date=Cast("created_at", DateField())) + .values("created_date") + .annotate(activity_count=Count("created_date")) + .order_by("created_date") + ) - assigned_issues = Issue.issue_objects.filter( - workspace__slug=slug, assignees__in=[request.user] - ).count() + month = request.GET.get("month", 1) - pending_issues_count = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, + completed_issues = ( + Issue.issue_objects.filter( assignees__in=[request.user], - ).count() - - completed_issues_count = Issue.issue_objects.filter( workspace__slug=slug, - assignees__in=[request.user], - state__group="completed", - ).count() + completed_at__month=month, + completed_at__isnull=False, + ) + .annotate(day_of_month=ExtractDay("completed_at")) + .annotate(week_in_month=WeekInMonth(F("day_of_month"))) + .values("week_in_month") + .annotate(completed_count=Count("id")) + .order_by("week_in_month") + ) - issues_due_week = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[request.user], - ) - .annotate(target_week=ExtractWeek("target_date")) - .filter(target_week=timezone.now().date().isocalendar()[1]) - .count() - ) + assigned_issues = Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] + ).count() - state_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, assignees__in=[request.user] - ) - .annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") - ) + pending_issues_count = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[request.user], + ).count() - overdue_issues = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[request.user], - target_date__lt=timezone.now(), - completed_at__isnull=True, - ).values("id", "name", "workspace__slug", "project_id", "target_date") + completed_issues_count = Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[request.user], + state__group="completed", + ).count() - upcoming_issues = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - start_date__gte=timezone.now(), + issues_due_week = ( + Issue.issue_objects.filter( workspace__slug=slug, assignees__in=[request.user], - completed_at__isnull=True, - ).values("id", "name", "workspace__slug", "project_id", "start_date") - - return Response( - { - "issue_activities": issue_activities, - "completed_issues": completed_issues, - "assigned_issues_count": assigned_issues, - "pending_issues_count": pending_issues_count, - "completed_issues_count": completed_issues_count, - "issues_due_week_count": issues_due_week, - "state_distribution": state_distribution, - "overdue_issues": overdue_issues, - "upcoming_issues": upcoming_issues, - }, - status=status.HTTP_200_OK, ) + .annotate(target_week=ExtractWeek("target_date")) + .filter(target_week=timezone.now().date().isocalendar()[1]) + .count() + ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + state_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] ) + .annotate(state_group=F("state__group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) + + overdue_issues = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[request.user], + target_date__lt=timezone.now(), + completed_at__isnull=True, + ).values("id", "name", "workspace__slug", "project_id", "target_date") + + upcoming_issues = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + start_date__gte=timezone.now(), + workspace__slug=slug, + assignees__in=[request.user], + completed_at__isnull=True, + ).values("id", "name", "workspace__slug", "project_id", "start_date") + + return Response( + { + "issue_activities": issue_activities, + "completed_issues": completed_issues, + "assigned_issues_count": assigned_issues, + "pending_issues_count": pending_issues_count, + "completed_issues_count": completed_issues_count, + "issues_due_week_count": issues_due_week, + "state_distribution": state_distribution, + "overdue_issues": overdue_issues, + "upcoming_issues": upcoming_issues, + }, + status=status.HTTP_200_OK, + ) class WorkspaceThemeViewSet(BaseViewSet): @@ -1034,157 +880,138 @@ def get_queryset(self): return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug")) def create(self, request, slug): - try: - workspace = Workspace.objects.get(slug=slug) - serializer = WorkspaceThemeSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(workspace=workspace, actor=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except Workspace.DoesNotExist: - return Response( - {"error": "Workspace does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + workspace = Workspace.objects.get(slug=slug) + serializer = WorkspaceThemeSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(workspace=workspace, actor=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class WorkspaceUserProfileStatsEndpoint(BaseAPIView): def get(self, request, slug, user_id): - try: - filters = issue_filters(request.query_params, "GET") + filters = issue_filters(request.query_params, "GET") - state_distribution = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - ) - .filter(**filters) - .annotate(state_group=F("state__group")) - .values("state_group") - .annotate(state_count=Count("state_group")) - .order_by("state_group") + state_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, ) + .filter(**filters) + .annotate(state_group=F("state__group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) - priority_order = ["urgent", "high", "medium", "low", None] - - priority_distribution = ( - Issue.objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - ) - .filter(**filters) - .values("priority") - .annotate(priority_count=Count("priority")) - .filter(priority_count__gte=1) - .annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - default=Value(len(priority_order)), - output_field=IntegerField(), - ) - ) - .order_by("priority_order") - ) + priority_order = ["urgent", "high", "medium", "low", "none"] - created_issues = ( - Issue.issue_objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - created_by_id=user_id, - ) - .filter(**filters) - .count() + priority_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, ) - - assigned_issues_count = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, + .filter(**filters) + .values("priority") + .annotate(priority_count=Count("priority")) + .filter(priority_count__gte=1) + .annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + default=Value(len(priority_order)), + output_field=IntegerField(), ) - .filter(**filters) - .count() ) + .order_by("priority_order") + ) - pending_issues_count = ( - Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - ) - .filter(**filters) - .count() + created_issues = ( + Issue.issue_objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + created_by_id=user_id, ) + .filter(**filters) + .count() + ) - completed_issues_count = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - state__group="completed", - project__project_projectmember__member=request.user, - ) - .filter(**filters) - .count() + assigned_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, ) + .filter(**filters) + .count() + ) - subscribed_issues_count = ( - IssueSubscriber.objects.filter( - workspace__slug=slug, - subscriber_id=user_id, - project__project_projectmember__member=request.user, - ) - .filter(**filters) - .count() + pending_issues_count = ( + Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, ) + .filter(**filters) + .count() + ) - upcoming_cycles = CycleIssue.objects.filter( + completed_issues_count = ( + Issue.issue_objects.filter( workspace__slug=slug, - cycle__start_date__gt=timezone.now().date(), - issue__assignees__in=[ - user_id, - ], - ).values("cycle__name", "cycle__id", "cycle__project_id") + assignees__in=[user_id], + state__group="completed", + project__project_projectmember__member=request.user, + ) + .filter(**filters) + .count() + ) - present_cycle = CycleIssue.objects.filter( + subscribed_issues_count = ( + IssueSubscriber.objects.filter( workspace__slug=slug, - cycle__start_date__lt=timezone.now().date(), - cycle__end_date__gt=timezone.now().date(), - issue__assignees__in=[ - user_id, - ], - ).values("cycle__name", "cycle__id", "cycle__project_id") - - return Response( - { - "state_distribution": state_distribution, - "priority_distribution": priority_distribution, - "created_issues": created_issues, - "assigned_issues": assigned_issues_count, - "completed_issues": completed_issues_count, - "pending_issues": pending_issues_count, - "subscribed_issues": subscribed_issues_count, - "present_cycles": present_cycle, - "upcoming_cycles": upcoming_cycles, - } - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + subscriber_id=user_id, + project__project_projectmember__member=request.user, ) + .filter(**filters) + .count() + ) + + upcoming_cycles = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__gt=timezone.now().date(), + issue__assignees__in=[ + user_id, + ], + ).values("cycle__name", "cycle__id", "cycle__project_id") + + present_cycle = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__lt=timezone.now().date(), + cycle__end_date__gt=timezone.now().date(), + issue__assignees__in=[ + user_id, + ], + ).values("cycle__name", "cycle__id", "cycle__project_id") + + return Response( + { + "state_distribution": state_distribution, + "priority_distribution": priority_distribution, + "created_issues": created_issues, + "assigned_issues": assigned_issues_count, + "completed_issues": completed_issues_count, + "pending_issues": pending_issues_count, + "subscribed_issues": subscribed_issues_count, + "present_cycles": present_cycle, + "upcoming_cycles": upcoming_cycles, + } + ) class WorkspaceUserActivityEndpoint(BaseAPIView): @@ -1193,119 +1020,116 @@ class WorkspaceUserActivityEndpoint(BaseAPIView): ] def get(self, request, slug, user_id): - try: - projects = request.query_params.getlist("project", []) - - queryset = IssueActivity.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - actor=user_id, - ).select_related("actor", "workspace", "issue", "project") - - if projects: - queryset = queryset.filter(project__in=projects) - - return self.paginate( - request=request, - queryset=queryset, - on_results=lambda issue_activities: IssueActivitySerializer( - issue_activities, many=True - ).data, - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + projects = request.query_params.getlist("project", []) + + queryset = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction", "draft"]), + workspace__slug=slug, + project__project_projectmember__member=request.user, + actor=user_id, + ).select_related("actor", "workspace", "issue", "project") + + if projects: + queryset = queryset.filter(project__in=projects) + + return self.paginate( + request=request, + queryset=queryset, + on_results=lambda issue_activities: IssueActivitySerializer( + issue_activities, many=True + ).data, + ) class WorkspaceUserProfileEndpoint(BaseAPIView): def get(self, request, slug, user_id): - try: - user_data = User.objects.get(pk=user_id) + user_data = User.objects.get(pk=user_id) - requesting_workspace_member = WorkspaceMember.objects.get( - workspace__slug=slug, member=request.user - ) - projects = [] - if requesting_workspace_member.role >= 10: - projects = ( - Project.objects.filter( - workspace__slug=slug, - project_projectmember__member=request.user, - ) - .annotate( - created_issues=Count( - "project_issue", - filter=Q(project_issue__created_by_id=user_id), - ) - ) - .annotate( - assigned_issues=Count( - "project_issue", - filter=Q(project_issue__assignees__in=[user_id]), - ) + requesting_workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + projects = [] + if requesting_workspace_member.role >= 10: + projects = ( + Project.objects.filter( + workspace__slug=slug, + project_projectmember__member=request.user, + ) + .annotate( + created_issues=Count( + "project_issue", + filter=Q( + project_issue__created_by_id=user_id, + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), ) - .annotate( - completed_issues=Count( - "project_issue", - filter=Q( - project_issue__completed_at__isnull=False, - project_issue__assignees__in=[user_id], - ), - ) + ) + .annotate( + assigned_issues=Count( + "project_issue", + filter=Q( + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), ) - .annotate( - pending_issues=Count( - "project_issue", - filter=Q( - project_issue__state__group__in=[ - "backlog", - "unstarted", - "started", - ], - project_issue__assignees__in=[user_id], - ), - ) + ) + .annotate( + completed_issues=Count( + "project_issue", + filter=Q( + project_issue__completed_at__isnull=False, + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), ) - .values( - "id", - "name", - "identifier", - "emoji", - "icon_prop", - "created_issues", - "assigned_issues", - "completed_issues", - "pending_issues", + ) + .annotate( + pending_issues=Count( + "project_issue", + filter=Q( + project_issue__state__group__in=[ + "backlog", + "unstarted", + "started", + ], + project_issue__assignees__in=[user_id], + project_issue__archived_at__isnull=True, + project_issue__is_draft=False, + ), ) ) + .values( + "id", + "name", + "identifier", + "emoji", + "icon_prop", + "created_issues", + "assigned_issues", + "completed_issues", + "pending_issues", + ) + ) - return Response( - { - "project_data": projects, - "user_data": { - "email": user_data.email, - "first_name": user_data.first_name, - "last_name": user_data.last_name, - "avatar": user_data.avatar, - "cover_image": user_data.cover_image, - "date_joined": user_data.date_joined, - "user_timezone": user_data.user_timezone, - "display_name": user_data.display_name, - }, + return Response( + { + "project_data": projects, + "user_data": { + "email": user_data.email, + "first_name": user_data.first_name, + "last_name": user_data.last_name, + "avatar": user_data.avatar, + "cover_image": user_data.cover_image, + "date_joined": user_data.date_joined, + "user_timezone": user_data.user_timezone, + "display_name": user_data.display_name, }, - status=status.HTTP_200_OK, - ) - except WorkspaceMember.DoesNotExist: - return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + }, + status=status.HTTP_200_OK, + ) class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): @@ -1314,124 +1138,122 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): ] def get(self, request, slug, user_id): - try: - filters = issue_filters(request.query_params, "GET") - order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = ( - Issue.issue_objects.filter( - Q(assignees__in=[user_id]) - | Q(created_by_id=user_id) - | Q(issue_subscribers__subscriber_id=user_id), - workspace__slug=slug, - project__project_projectmember__member=request.user, - ) - .filter(**filters) - .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .select_related("project", "workspace", "state", "parent") - .prefetch_related("assignees", "labels") - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) - ) - .order_by("-created_at") - .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") + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + issue_queryset = ( + Issue.issue_objects.filter( + Q(assignees__in=[user_id]) + | Q(created_by_id=user_id) + | Q(issue_subscribers__subscriber_id=user_id), + workspace__slug=slug, + project__project_projectmember__member=request.user, + ) + .filter(**filters) + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .select_related("project", "workspace", "state", "parent") + .prefetch_related("assignees", "labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), ) - ).distinct() - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] + ) + .order_by("-created_at") + .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() + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order if order_by_param == "priority" else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issues = IssueLiteSerializer(issue_queryset, many=True).data + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) - ## Grouping the results - group_by = request.GET.get("group_by", False) - if group_by: - return Response( - group_results(issues, group_by), status=status.HTTP_200_OK - ) + issues = IssueLiteSerializer(issue_queryset, many=True).data - return Response(issues, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + grouped_results = group_results(issues, group_by) return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, + grouped_results, + status=status.HTTP_200_OK, ) + return Response( + issues, status=status.HTTP_200_OK + ) + class WorkspaceLabelsEndpoint(BaseAPIView): permission_classes = [ @@ -1439,36 +1261,35 @@ class WorkspaceLabelsEndpoint(BaseAPIView): ] def get(self, request, slug): - try: - labels = Label.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - ).values("parent", "name", "color", "id", "project_id", "workspace__slug") - return Response(labels, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) + labels = Label.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + ).values("parent", "name", "color", "id", "project_id", "workspace__slug") + return Response(labels, status=status.HTTP_200_OK) -class WorkspaceMembersEndpoint(BaseAPIView): +class LeaveWorkspaceEndpoint(BaseAPIView): permission_classes = [ WorkspaceEntityPermission, ] - def get(self, request, slug): - try: - workspace_members = WorkspaceMember.objects.filter( - workspace__slug=slug, - member__is_bot=False, - ).select_related("workspace", "member") - serialzier = WorkSpaceMemberSerializer(workspace_members, many=True) - return Response(serialzier.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) + def delete(self, request, slug): + workspace_member = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + + # Only Admin case + if ( + workspace_member.role == 20 + and WorkspaceMember.objects.filter(workspace__slug=slug, role=20).count() + == 1 + ): return Response( - {"error": "Something went wrong please try again later"}, + { + "error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace" + }, status=status.HTTP_400_BAD_REQUEST, ) + # Delete the member from workspace + workspace_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index 492be887071..a80770c3723 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -20,8 +20,8 @@ row_mapping = { "state__name": "State", "state__group": "State Group", - "labels__name": "Label", - "assignees__display_name": "Assignee Name", + "labels__id": "Label", + "assignees__id": "Assignee Name", "start_date": "Start Date", "target_date": "Due Date", "completed_at": "Completed At", @@ -29,8 +29,321 @@ "issue_count": "Issue Count", "priority": "Priority", "estimate": "Estimate", + "issue_cycle__cycle_id": "Cycle", + "issue_module__module_id": "Module" } +ASSIGNEE_ID = "assignees__id" +LABEL_ID = "labels__id" +STATE_ID = "state_id" +CYCLE_ID = "issue_cycle__cycle_id" +MODULE_ID = "issue_module__module_id" + + +def send_export_email(email, slug, csv_buffer): + """Helper function to send export email.""" + subject = "Your Export is ready" + html_content = render_to_string("emails/exports/analytics.html", {}) + text_content = strip_tags(html_content) + + csv_buffer.seek(0) + msg = EmailMultiAlternatives(subject, text_content, settings.EMAIL_FROM, [email]) + msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue()) + msg.send(fail_silently=False) + + +def get_assignee_details(slug, filters): + """Fetch assignee details if required.""" + return ( + Issue.issue_objects.filter( + workspace__slug=slug, **filters, assignees__avatar__isnull=False + ) + .distinct("assignees__id") + .order_by("assignees__id") + .values( + "assignees__avatar", + "assignees__display_name", + "assignees__first_name", + "assignees__last_name", + "assignees__id", + ) + ) + + +def get_label_details(slug, filters): + """Fetch label details if required""" + return ( + Issue.objects.filter(workspace__slug=slug, **filters, labels__id__isnull=False) + .distinct("labels__id") + .order_by("labels__id") + .values("labels__id", "labels__color", "labels__name") + ) + + +def get_state_details(slug, filters): + return ( + Issue.issue_objects.filter( + workspace__slug=slug, + **filters, + ) + .distinct("state_id") + .order_by("state_id") + .values("state_id", "state__name", "state__color") + ) + + +def get_module_details(slug, filters): + return ( + Issue.issue_objects.filter( + workspace__slug=slug, + **filters, + issue_module__module_id__isnull=False, + ) + .distinct("issue_module__module_id") + .order_by("issue_module__module_id") + .values( + "issue_module__module_id", + "issue_module__module__name", + ) + ) + + +def get_cycle_details(slug, filters): + return ( + Issue.issue_objects.filter( + workspace__slug=slug, + **filters, + issue_cycle__cycle_id__isnull=False, + ) + .distinct("issue_cycle__cycle_id") + .order_by("issue_cycle__cycle_id") + .values( + "issue_cycle__cycle_id", + "issue_cycle__cycle__name", + ) + ) + + +def generate_csv_from_rows(rows): + """Generate CSV buffer from rows.""" + csv_buffer = io.StringIO() + writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) + [writer.writerow(row) for row in rows] + return csv_buffer + + +def generate_segmented_rows( + distribution, + x_axis, + y_axis, + segment, + key, + assignee_details, + label_details, + state_details, + cycle_details, + module_details, +): + segment_zero = list( + set( + item.get("segment") for sublist in distribution.values() for item in sublist + ) + ) + + segmented = segment + + row_zero = [ + row_mapping.get(x_axis, "X-Axis"), + row_mapping.get(y_axis, "Y-Axis"), + ] + segment_zero + + rows = [] + for item, data in distribution.items(): + generated_row = [ + item, + sum(obj.get(key) for obj in data if obj.get(key) is not None), + ] + + for segment in segment_zero: + value = next((x.get(key) for x in data if x.get("segment") == segment), "0") + generated_row.append(value) + + if x_axis == ASSIGNEE_ID: + assignee = next( + ( + user + for user in assignee_details + if str(user[ASSIGNEE_ID]) == str(item) + ), + None, + ) + if assignee: + generated_row[ + 0 + ] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" + + if x_axis == LABEL_ID: + label = next( + (lab for lab in label_details if str(lab[LABEL_ID]) == str(item)), + None, + ) + + if label: + generated_row[0] = f"{label['labels__name']}" + + if x_axis == STATE_ID: + state = next( + (sta for sta in state_details if str(sta[STATE_ID]) == str(item)), + None, + ) + + if state: + generated_row[0] = f"{state['state__name']}" + + if x_axis == CYCLE_ID: + cycle = next( + (cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)), + None, + ) + + if cycle: + generated_row[0] = f"{cycle['issue_cycle__cycle__name']}" + + if x_axis == MODULE_ID: + module = next( + (mod for mod in module_details if str(mod[MODULE_ID]) == str(item)), + None, + ) + + if module: + generated_row[0] = f"{module['issue_module__module__name']}" + + rows.append(tuple(generated_row)) + + if segmented == ASSIGNEE_ID: + for index, segm in enumerate(row_zero[2:]): + assignee = next( + ( + user + for user in assignee_details + if str(user[ASSIGNEE_ID]) == str(segm) + ), + None, + ) + if assignee: + row_zero[ + index + 2 + ] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" + + if segmented == LABEL_ID: + for index, segm in enumerate(row_zero[2:]): + label = next( + (lab for lab in label_details if str(lab[LABEL_ID]) == str(segm)), + None, + ) + if label: + row_zero[index + 2] = label["labels__name"] + + if segmented == STATE_ID: + for index, segm in enumerate(row_zero[2:]): + state = next( + (sta for sta in state_details if str(sta[STATE_ID]) == str(segm)), + None, + ) + if state: + row_zero[index + 2] = state["state__name"] + + if segmented == MODULE_ID: + for index, segm in enumerate(row_zero[2:]): + module = next( + (mod for mod in label_details if str(mod[MODULE_ID]) == str(segm)), + None, + ) + if module: + row_zero[index + 2] = module["issue_module__module__name"] + + if segmented == CYCLE_ID: + for index, segm in enumerate(row_zero[2:]): + cycle = next( + (cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(segm)), + None, + ) + if cycle: + row_zero[index + 2] = cycle["issue_cycle__cycle__name"] + + return [tuple(row_zero)] + rows + + +def generate_non_segmented_rows( + distribution, + x_axis, + y_axis, + key, + assignee_details, + label_details, + state_details, + cycle_details, + module_details, +): + rows = [] + for item, data in distribution.items(): + row = [item, data[0].get("count" if y_axis == "issue_count" else "estimate")] + + if x_axis == ASSIGNEE_ID: + assignee = next( + ( + user + for user in assignee_details + if str(user[ASSIGNEE_ID]) == str(item) + ), + None, + ) + if assignee: + row[ + 0 + ] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" + + if x_axis == LABEL_ID: + label = next( + (lab for lab in label_details if str(lab[LABEL_ID]) == str(item)), + None, + ) + + if label: + row[0] = f"{label['labels__name']}" + + if x_axis == STATE_ID: + state = next( + (sta for sta in state_details if str(sta[STATE_ID]) == str(item)), + None, + ) + + if state: + row[0] = f"{state['state__name']}" + + if x_axis == CYCLE_ID: + cycle = next( + (cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)), + None, + ) + + if cycle: + row[0] = f"{cycle['issue_cycle__cycle__name']}" + + if x_axis == MODULE_ID: + module = next( + (mod for mod in module_details if str(mod[MODULE_ID]) == str(item)), + None, + ) + + if module: + row[0] = f"{module['issue_module__module__name']}" + + rows.append(tuple(row)) + + row_zero = [row_mapping.get(x_axis, "X-Axis"), row_mapping.get(y_axis, "Y-Axis")] + return [tuple(row_zero)] + rows + @shared_task def analytic_export_task(email, data, slug): @@ -43,134 +356,69 @@ def analytic_export_task(email, data, slug): segment = data.get("segment", False) distribution = build_graph_plot( - queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment + queryset, x_axis=x_axis, y_axis=y_axis, segment=segment ) - key = "count" if y_axis == "issue_count" else "estimate" - segmented = segment + assignee_details = ( + get_assignee_details(slug, filters) + if x_axis == ASSIGNEE_ID or segment == ASSIGNEE_ID + else {} + ) - assignee_details = {} - if x_axis in ["assignees__id"] or segment in ["assignees__id"]: - assignee_details = ( - Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) - .order_by("assignees__id") - .distinct("assignees__id") - .values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id") - ) + label_details = ( + get_label_details(slug, filters) + if x_axis == LABEL_ID or segment == LABEL_ID + else {} + ) - if segment: - segment_zero = [] - for item in distribution: - current_dict = distribution.get(item) - for current in current_dict: - segment_zero.append(current.get("segment")) - - segment_zero = list(set(segment_zero)) - row_zero = ( - [ - row_mapping.get(x_axis, "X-Axis"), - ] - + [ - row_mapping.get(y_axis, "Y-Axis"), - ] - + segment_zero - ) - rows = [] - for item in distribution: - generated_row = [ - item, - ] - - data = distribution.get(item) - # Add y axis values - generated_row.append(sum(obj.get(key) for obj in data if obj.get(key, None) is not None)) - - for segment in segment_zero: - value = [x for x in data if x.get("segment") == segment] - if len(value): - generated_row.append(value[0].get(key)) - else: - generated_row.append("0") - # x-axis replacement for names - if x_axis in ["assignees__id"]: - assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)] - if len(assignee): - generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name")) - rows.append(tuple(generated_row)) - - # If segment is ["assignees__display_name"] then replace segment_zero rows with first and last names - if segmented in ["assignees__id"]: - for index, segm in enumerate(row_zero[2:]): - # find the name of the user - assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(segm)] - if len(assignee): - row_zero[index + 2] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name")) - - rows = [tuple(row_zero)] + rows - csv_buffer = io.StringIO() - writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) - - # Write CSV data to the buffer - for row in rows: - writer.writerow(row) - - subject = "Your Export is ready" - - html_content = render_to_string("emails/exports/analytics.html", {}) - - text_content = strip_tags(html_content) - csv_buffer.seek(0) - msg = EmailMultiAlternatives( - subject, text_content, settings.EMAIL_FROM, [email] - ) - msg.attach(f"{slug}-analytics.csv", csv_buffer.read()) - msg.send(fail_silently=False) + state_details = ( + get_state_details(slug, filters) + if x_axis == STATE_ID or segment == STATE_ID + else {} + ) + + cycle_details = ( + get_cycle_details(slug, filters) + if x_axis == CYCLE_ID or segment == CYCLE_ID + else {} + ) + module_details = ( + get_module_details(slug, filters) + if x_axis == MODULE_ID or segment == MODULE_ID + else {} + ) + + if segment: + rows = generate_segmented_rows( + distribution, + x_axis, + y_axis, + segment, + key, + assignee_details, + label_details, + state_details, + cycle_details, + module_details, + ) else: - row_zero = [ - row_mapping.get(x_axis, "X-Axis"), - row_mapping.get(y_axis, "Y-Axis"), - ] - rows = [] - for item in distribution: - row = [ - item, - distribution.get(item)[0].get("count") - if y_axis == "issue_count" - else distribution.get(item)[0].get("estimate "), - ] - # x-axis replacement to names - if x_axis in ["assignees__id"]: - assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)] - if len(assignee): - row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name")) - - rows.append(tuple(row)) - rows = [tuple(row_zero)] + rows - csv_buffer = io.StringIO() - writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) - - # Write CSV data to the buffer - for row in rows: - writer.writerow(row) - - subject = "Your Export is ready" - - html_content = render_to_string("emails/exports/analytics.html", {}) - - text_content = strip_tags(html_content) - - csv_buffer.seek(0) - msg = EmailMultiAlternatives( - subject, text_content, settings.EMAIL_FROM, [email] - ) - msg.attach(f"{slug}-analytics.csv", csv_buffer.read()) - msg.send(fail_silently=False) + rows = generate_non_segmented_rows( + distribution, + x_axis, + y_axis, + key, + assignee_details, + label_details, + state_details, + cycle_details, + module_details, + ) + csv_buffer = generate_csv_from_rows(rows) + send_export_email(email, slug, csv_buffer) except Exception as e: - # Print logs if in DEBUG mode if settings.DEBUG: print(e) capture_exception(e) - return diff --git a/apiserver/plane/bgtasks/email_verification_task.py b/apiserver/plane/bgtasks/email_verification_task.py index 93b15c425fa..9f9d064373b 100644 --- a/apiserver/plane/bgtasks/email_verification_task.py +++ b/apiserver/plane/bgtasks/email_verification_task.py @@ -23,7 +23,7 @@ def email_verification(first_name, email, token, current_site): from_email_string = settings.EMAIL_FROM - subject = f"Verify your Email!" + subject = "Verify your Email!" context = { "first_name": first_name, diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index a45120eb5dd..1329697e904 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -4,7 +4,6 @@ import json import boto3 import zipfile -from urllib.parse import urlparse, urlunparse # Django imports from django.conf import settings diff --git a/apiserver/plane/bgtasks/exporter_expired_task.py b/apiserver/plane/bgtasks/exporter_expired_task.py index a77d68b4b55..45c53eaca05 100644 --- a/apiserver/plane/bgtasks/exporter_expired_task.py +++ b/apiserver/plane/bgtasks/exporter_expired_task.py @@ -32,7 +32,7 @@ def delete_old_s3_link(): else: s3 = boto3.client( "s3", - region_name="ap-south-1", + region_name=settings.AWS_REGION, aws_access_key_id=settings.AWS_ACCESS_KEY_ID, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, config=Config(signature_version="s3v4"), diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index 93283dfd540..de1390f016f 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -8,20 +8,18 @@ from celery import shared_task from sentry_sdk import capture_exception -# Module imports -from plane.db.models import User @shared_task def forgot_password(first_name, email, uidb64, token, current_site): try: - realtivelink = f"/reset-password/?uidb64={uidb64}&token={token}" + realtivelink = f"/accounts/reset-password/?uidb64={uidb64}&token={token}" abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM - subject = f"Reset Your Password - Plane" + subject = "Reset Your Password - Plane" context = { "first_name": first_name, diff --git a/apiserver/plane/bgtasks/importer_task.py b/apiserver/plane/bgtasks/importer_task.py index 757ef601b8f..14bece21b0b 100644 --- a/apiserver/plane/bgtasks/importer_task.py +++ b/apiserver/plane/bgtasks/importer_task.py @@ -2,8 +2,6 @@ import json import requests import uuid -import jwt -from datetime import datetime # Django imports from django.conf import settings @@ -25,8 +23,8 @@ WorkspaceIntegration, Label, User, + IssueProperty, ) -from .workspace_invitation_task import workspace_invitation from plane.bgtasks.user_welcome_task import send_welcome_slack @@ -57,7 +55,7 @@ def service_importer(service, importer_id): ignore_conflicts=True, ) - [ + _ = [ send_welcome_slack.delay( str(user.id), True, @@ -103,6 +101,20 @@ def service_importer(service, importer_id): ignore_conflicts=True, ) + IssueProperty.objects.bulk_create( + [ + IssueProperty( + project_id=importer.project_id, + workspace_id=importer.workspace_id, + user=user, + created_by=importer.created_by, + ) + for user in workspace_users + ], + batch_size=100, + ignore_conflicts=True, + ) + # Check if sync config is on for github importers if service == "github" and importer.config.get("sync", False): name = importer.metadata.get("name", False) @@ -142,7 +154,7 @@ def service_importer(service, importer_id): ) # Create repo sync - repo_sync = GithubRepositorySync.objects.create( + _ = GithubRepositorySync.objects.create( repository=repo, workspace_integration=workspace_integration, actor=workspace_integration.actor, @@ -164,7 +176,7 @@ def service_importer(service, importer_id): ImporterSerializer(importer).data, cls=DjangoJSONEncoder, ) - res = requests.post( + _ = requests.post( f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/", json=import_data_json, headers=headers, diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 1cc6c85cc9a..f0a20eeec39 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -24,132 +24,160 @@ IssueSubscriber, Notification, IssueAssignee, + IssueReaction, + CommentReaction, + IssueComment, ) from plane.api.serializers import IssueActivitySerializer +from plane.bgtasks.notification_task import notifications -# Track Chnages in name +# Track Changes in name def track_name( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): if current_instance.get("name") != requested_data.get("name"): issue_activities.append( IssueActivity( issue_id=issue_id, - actor=actor, + actor_id=actor_id, verb="updated", old_value=current_instance.get("name"), new_value=requested_data.get("name"), field="name", - project=project, - workspace=project.workspace, - comment=f"updated the name to {requested_data.get('name')}", + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated the name to", + epoch=epoch, ) ) -# Track changes in parent issue -def track_parent( +# Track issue description +def track_description( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): - if current_instance.get("parent") != requested_data.get("parent"): - if requested_data.get("parent") == None: - old_parent = Issue.objects.get(pk=current_instance.get("parent")) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=f"{old_parent.project.identifier}-{old_parent.sequence_id}", - new_value=None, - field="parent", - project=project, - workspace=project.workspace, - comment=f"updated the parent issue to None", - old_identifier=old_parent.id, - new_identifier=None, - ) - ) + if current_instance.get("description_html") != requested_data.get( + "description_html" + ): + last_activity = ( + IssueActivity.objects.filter(issue_id=issue_id) + .order_by("-created_at") + .first() + ) + if ( + last_activity is not None + and last_activity.field == "description" + and actor_id == str(last_activity.actor_id) + ): + last_activity.created_at = timezone.now() + last_activity.save(update_fields=["created_at"]) else: - new_parent = Issue.objects.get(pk=requested_data.get("parent")) - old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() issue_activities.append( IssueActivity( issue_id=issue_id, - actor=actor, + actor_id=actor_id, verb="updated", - old_value=f"{old_parent.project.identifier}-{old_parent.sequence_id}" - if old_parent is not None - else None, - new_value=f"{new_parent.project.identifier}-{new_parent.sequence_id}", - field="parent", - project=project, - workspace=project.workspace, - comment=f"updated the parent issue to {new_parent.name}", - old_identifier=old_parent.id if old_parent is not None else None, - new_identifier=new_parent.id, + old_value=current_instance.get("description_html"), + new_value=requested_data.get("description_html"), + field="description", + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated the description to", + epoch=epoch, ) ) +# Track changes in parent issue +def track_parent( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + if current_instance.get("parent") != requested_data.get("parent"): + old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() + new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first() + + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="updated", + old_value=f"{old_parent.project.identifier}-{old_parent.sequence_id}" + if old_parent is not None + else "", + new_value=f"{new_parent.project.identifier}-{new_parent.sequence_id}" + if new_parent is not None + else "", + field="parent", + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated the parent issue to", + old_identifier=old_parent.id if old_parent is not None else None, + new_identifier=new_parent.id if new_parent is not None else None, + epoch=epoch, + ) + ) + + # Track changes in priority def track_priority( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): if current_instance.get("priority") != requested_data.get("priority"): - if requested_data.get("priority") == None: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("priority"), - new_value=None, - field="priority", - project=project, - workspace=project.workspace, - comment=f"updated the priority to None", - ) - ) - else: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("priority"), - new_value=requested_data.get("priority"), - field="priority", - project=project, - workspace=project.workspace, - comment=f"updated the priority to {requested_data.get('priority')}", - ) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="updated", + old_value=current_instance.get("priority"), + new_value=requested_data.get("priority"), + field="priority", + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated the priority to", + epoch=epoch, ) + ) -# Track chnages in state of the issue +# Track changes in state of the issue def track_state( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): if current_instance.get("state") != requested_data.get("state"): new_state = State.objects.get(pk=requested_data.get("state", None)) @@ -158,90 +186,51 @@ def track_state( issue_activities.append( IssueActivity( issue_id=issue_id, - actor=actor, + actor_id=actor_id, verb="updated", old_value=old_state.name, new_value=new_state.name, field="state", - project=project, - workspace=project.workspace, - comment=f"updated the state to {new_state.name}", + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated the state to", old_identifier=old_state.id, new_identifier=new_state.id, + epoch=epoch, ) ) -# Track issue description -def track_description( - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, -): - if current_instance.get("description_html") != requested_data.get( - "description_html" - ): - last_activity = IssueActivity.objects.filter(issue_id=issue_id).order_by("-created_at").first() - if(last_activity is not None and last_activity.field == "description" and actor.id == last_activity.actor_id): - last_activity.created_at = timezone.now() - last_activity.save(update_fields=["created_at"]) - else: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("description_html"), - new_value=requested_data.get("description_html"), - field="description", - project=project, - workspace=project.workspace, - comment=f"updated the description to {requested_data.get('description_html')}", - ) - ) - - # Track changes in issue target date def track_target_date( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): if current_instance.get("target_date") != requested_data.get("target_date"): - if requested_data.get("target_date") == None: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("target_date"), - new_value=requested_data.get("target_date"), - field="target_date", - project=project, - workspace=project.workspace, - comment=f"updated the target date to None", - ) - ) - else: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("target_date"), - new_value=requested_data.get("target_date"), - field="target_date", - project=project, - workspace=project.workspace, - comment=f"updated the target date to {requested_data.get('target_date')}", - ) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="updated", + old_value=current_instance.get("target_date") + if current_instance.get("target_date") is not None + else "", + new_value=requested_data.get("target_date") + if requested_data.get("target_date") is not None + else "", + field="target_date", + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated the target date to", + epoch=epoch, ) + ) # Track changes in issue start date @@ -249,39 +238,31 @@ def track_start_date( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): if current_instance.get("start_date") != requested_data.get("start_date"): - if requested_data.get("start_date") == None: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("start_date"), - new_value=requested_data.get("start_date"), - field="start_date", - project=project, - workspace=project.workspace, - comment=f"updated the start date to None", - ) - ) - else: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=current_instance.get("start_date"), - new_value=requested_data.get("start_date"), - field="start_date", - project=project, - workspace=project.workspace, - comment=f"updated the start date to {requested_data.get('start_date')}", - ) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="updated", + old_value=current_instance.get("start_date") + if current_instance.get("start_date") is not None + else "", + new_value=requested_data.get("start_date") + if requested_data.get("start_date") is not None + else "", + field="start_date", + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated the start date to ", + epoch=epoch, ) + ) # Track changes in issue labels @@ -289,51 +270,57 @@ def track_labels( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): - # Label Addition - if len(requested_data.get("labels_list")) > len(current_instance.get("labels")): - for label in requested_data.get("labels_list"): - if label not in current_instance.get("labels"): - label = Label.objects.get(pk=label) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value="", - new_value=label.name, - field="labels", - project=project, - workspace=project.workspace, - comment=f"added label {label.name}", - new_identifier=label.id, - old_identifier=None, - ) - ) + requested_labels = set([str(lab) for lab in requested_data.get("labels", [])]) + current_labels = set([str(lab) for lab in current_instance.get("labels", [])]) - # Label Removal - if len(requested_data.get("labels_list")) < len(current_instance.get("labels")): - for label in current_instance.get("labels"): - if label not in requested_data.get("labels_list"): - label = Label.objects.get(pk=label) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=label.name, - new_value="", - field="labels", - project=project, - workspace=project.workspace, - comment=f"removed label {label.name}", - old_identifier=label.id, - new_identifier=None, - ) - ) + added_labels = requested_labels - current_labels + dropped_labels = current_labels - requested_labels + + # Set of newly added labels + for added_label in added_labels: + label = Label.objects.get(pk=added_label) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + project_id=project_id, + workspace_id=workspace_id, + verb="updated", + field="labels", + comment="added label ", + old_value="", + new_value=label.name, + new_identifier=label.id, + old_identifier=None, + epoch=epoch, + ) + ) + + # Set of dropped labels + for dropped_label in dropped_labels: + label = Label.objects.get(pk=dropped_label) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="updated", + old_value=label.name, + new_value="", + field="labels", + project_id=project_id, + workspace_id=workspace_id, + comment=f"removed label ", + old_identifier=label.id, + new_identifier=None, + epoch=epoch, + ) + ) # Track changes in issue assignees @@ -341,301 +328,204 @@ def track_assignees( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): - # Assignee Addition - if len(requested_data.get("assignees_list")) > len( - current_instance.get("assignees") - ): - for assignee in requested_data.get("assignees_list"): - if assignee not in current_instance.get("assignees"): - assignee = User.objects.get(pk=assignee) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value="", - new_value=assignee.display_name, - field="assignees", - project=project, - workspace=project.workspace, - comment=f"added assignee {assignee.display_name}", - new_identifier=assignee.id, - ) - ) + requested_assignees = set([str(asg) for asg in requested_data.get("assignees", [])]) + current_assignees = set([str(asg) for asg in current_instance.get("assignees", [])]) - # Assignee Removal - if len(requested_data.get("assignees_list")) < len( - current_instance.get("assignees") - ): - for assignee in current_instance.get("assignees"): - if assignee not in requested_data.get("assignees_list"): - assignee = User.objects.get(pk=assignee) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=assignee.display_name, - new_value="", - field="assignees", - project=project, - workspace=project.workspace, - comment=f"removed assignee {assignee.display_name}", - old_identifier=assignee.id, - ) - ) + added_assignees = requested_assignees - current_assignees + dropped_assginees = current_assignees - requested_assignees + + for added_asignee in added_assignees: + assignee = User.objects.get(pk=added_asignee) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="updated", + old_value="", + new_value=assignee.display_name, + field="assignees", + project_id=project_id, + workspace_id=workspace_id, + comment=f"added assignee ", + new_identifier=assignee.id, + epoch=epoch, + ) + ) + + for dropped_assignee in dropped_assginees: + assignee = User.objects.get(pk=dropped_assignee) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="updated", + old_value=assignee.display_name, + new_value="", + field="assignees", + project_id=project_id, + workspace_id=workspace_id, + comment=f"removed assignee ", + old_identifier=assignee.id, + epoch=epoch, + ) + ) -# Track changes in blocking issues -def track_blocks( +def track_estimate_points( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): - if len(requested_data.get("blocks_list")) > len( - current_instance.get("blocked_issues") - ): - for block in requested_data.get("blocks_list"): - if ( - len( - [ - blocked - for blocked in current_instance.get("blocked_issues") - if blocked.get("block") == block - ] - ) - == 0 - ): - issue = Issue.objects.get(pk=block) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value="", - new_value=f"{issue.project.identifier}-{issue.sequence_id}", - field="blocks", - project=project, - workspace=project.workspace, - comment=f"added blocking issue {project.identifier}-{issue.sequence_id}", - new_identifier=issue.id, - ) - ) - - # Blocked Issue Removal - if len(requested_data.get("blocks_list")) < len( - current_instance.get("blocked_issues") - ): - for blocked in current_instance.get("blocked_issues"): - if blocked.get("block") not in requested_data.get("blocks_list"): - issue = Issue.objects.get(pk=blocked.get("block")) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=f"{issue.project.identifier}-{issue.sequence_id}", - new_value="", - field="blocks", - project=project, - workspace=project.workspace, - comment=f"removed blocking issue {project.identifier}-{issue.sequence_id}", - old_identifier=issue.id, - ) - ) + if current_instance.get("estimate_point") != requested_data.get("estimate_point"): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="updated", + old_value=current_instance.get("estimate_point") + if current_instance.get("estimate_point") is not None + else "", + new_value=requested_data.get("estimate_point") + if requested_data.get("estimate_point") is not None + else "", + field="estimate_point", + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated the estimate point to ", + epoch=epoch, + ) + ) -# Track changes in blocked_by issues -def track_blockings( +def track_archive_at( requested_data, current_instance, issue_id, - project, - actor, + project_id, + workspace_id, + actor_id, issue_activities, + epoch, ): - if len(requested_data.get("blockers_list")) > len( - current_instance.get("blocker_issues") - ): - for block in requested_data.get("blockers_list"): - if ( - len( - [ - blocked - for blocked in current_instance.get("blocker_issues") - if blocked.get("blocked_by") == block - ] - ) - == 0 - ): - issue = Issue.objects.get(pk=block) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value="", - new_value=f"{issue.project.identifier}-{issue.sequence_id}", - field="blocking", - project=project, - workspace=project.workspace, - comment=f"added blocked by issue {project.identifier}-{issue.sequence_id}", - new_identifier=issue.id, - ) - ) - - # Blocked Issue Removal - if len(requested_data.get("blockers_list")) < len( - current_instance.get("blocker_issues") - ): - for blocked in current_instance.get("blocker_issues"): - if blocked.get("blocked_by") not in requested_data.get("blockers_list"): - issue = Issue.objects.get(pk=blocked.get("blocked_by")) - issue_activities.append( - IssueActivity( - issue_id=issue_id, - actor=actor, - verb="updated", - old_value=f"{issue.project.identifier}-{issue.sequence_id}", - new_value="", - field="blocking", - project=project, - workspace=project.workspace, - comment=f"removed blocked by issue {project.identifier}-{issue.sequence_id}", - old_identifier=issue.id, - ) - ) - - -def create_issue_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities -): - issue_activities.append( - IssueActivity( - issue_id=issue_id, - project=project, - workspace=project.workspace, - comment=f"created the issue", - verb="created", - actor=actor, - ) - ) - - -def track_estimate_points( - requested_data, current_instance, issue_id, project, actor, issue_activities -): - if current_instance.get("estimate_point") != requested_data.get("estimate_point"): - if requested_data.get("estimate_point") == None: + if current_instance.get("archived_at") != requested_data.get("archived_at"): + if requested_data.get("archived_at") is None: issue_activities.append( IssueActivity( issue_id=issue_id, - actor=actor, + project_id=project_id, + workspace_id=workspace_id, + comment="has restored the issue", verb="updated", - old_value=current_instance.get("estimate_point"), - new_value=requested_data.get("estimate_point"), - field="estimate_point", - project=project, - workspace=project.workspace, - comment=f"updated the estimate point to None", + actor_id=actor_id, + field="archived_at", + old_value="archive", + new_value="restore", + epoch=epoch, ) ) else: issue_activities.append( IssueActivity( issue_id=issue_id, - actor=actor, + project_id=project_id, + workspace_id=workspace_id, + comment="Plane has archived the issue", verb="updated", - old_value=current_instance.get("estimate_point"), - new_value=requested_data.get("estimate_point"), - field="estimate_point", - project=project, - workspace=project.workspace, - comment=f"updated the estimate point to {requested_data.get('estimate_point')}", + actor_id=actor_id, + field="archived_at", + old_value=None, + new_value="archive", + epoch=epoch, ) ) -def track_archive_at( - requested_data, current_instance, issue_id, project, actor, issue_activities -): - if requested_data.get("archived_at") is None: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - project=project, - workspace=project.workspace, - comment=f"has restored the issue", - verb="updated", - actor=actor, - field="archived_at", - old_value="archive", - new_value="restore", - ) - ) - else: - issue_activities.append( - IssueActivity( - issue_id=issue_id, - project=project, - workspace=project.workspace, - comment=f"Plane has archived the issue", - verb="updated", - actor=actor, - field="archived_at", - old_value=None, - new_value="archive", - ) - ) - - def track_closed_to( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): if requested_data.get("closed_to") is not None: updated_state = State.objects.get( - pk=requested_data.get("closed_to"), project=project + pk=requested_data.get("closed_to"), project_id=project_id ) - issue_activities.append( IssueActivity( issue_id=issue_id, - actor=actor, + actor_id=actor_id, verb="updated", old_value=None, new_value=updated_state.name, field="state", - project=project, - workspace=project.workspace, - comment=f"Plane updated the state to {updated_state.name}", + project_id=project_id, + workspace_id=workspace_id, + comment=f"Plane updated the state to ", old_identifier=None, new_identifier=updated_state.id, + epoch=epoch, ) ) +def create_issue_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project_id=project_id, + workspace_id=workspace_id, + comment=f"created the issue", + verb="created", + actor_id=actor_id, + epoch=epoch, + ) + ) + + def update_issue_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): ISSUE_ACTIVITY_MAPPER = { "name": track_name, "parent": track_parent, "priority": track_priority, "state": track_state, - "description": track_description, + "description_html": track_description, "target_date": track_target_date, "start_date": track_start_date, - "labels_list": track_labels, - "assignees_list": track_assignees, - "blocks_list": track_blocks, - "blockers_list": track_blockings, + "labels": track_labels, + "assignees": track_assignees, "estimate_point": track_estimate_points, "archived_at": track_archive_at, "closed_to": track_closed_to, @@ -647,35 +537,52 @@ def update_issue_activity( ) for key in requested_data: - func = ISSUE_ACTIVITY_MAPPER.get(key, None) + func = ISSUE_ACTIVITY_MAPPER.get(key) if func is not None: func( - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, + requested_data=requested_data, + current_instance=current_instance, + issue_id=issue_id, + project_id=project_id, + workspace_id=workspace_id, + actor_id=actor_id, + issue_activities=issue_activities, + epoch=epoch, ) def delete_issue_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities -): - issue_activities.append( - IssueActivity( - project=project, - workspace=project.workspace, - comment=f"deleted the issue", - verb="deleted", - actor=actor, + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + issue_activities.append( + IssueActivity( + project_id=project_id, + workspace_id=workspace_id, + comment=f"deleted the issue", + verb="deleted", + actor_id=actor_id, field="issue", + epoch=epoch, ) ) def create_comment_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( @@ -685,21 +592,29 @@ def create_comment_activity( issue_activities.append( IssueActivity( issue_id=issue_id, - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"created a comment", verb="created", - actor=actor, + actor_id=actor_id, field="comment", new_value=requested_data.get("comment_html", ""), new_identifier=requested_data.get("id", None), issue_comment_id=requested_data.get("id", None), + epoch=epoch, ) ) def update_comment_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( @@ -710,39 +625,55 @@ def update_comment_activity( issue_activities.append( IssueActivity( issue_id=issue_id, - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"updated a comment", verb="updated", - actor=actor, + actor_id=actor_id, field="comment", old_value=current_instance.get("comment_html", ""), old_identifier=current_instance.get("id"), new_value=requested_data.get("comment_html", ""), new_identifier=current_instance.get("id", None), issue_comment_id=current_instance.get("id", None), + epoch=epoch, ) ) def delete_comment_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): issue_activities.append( IssueActivity( issue_id=issue_id, - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"deleted the comment", verb="deleted", - actor=actor, + actor_id=actor_id, field="comment", + epoch=epoch, ) ) def create_cycle_issue_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( @@ -764,16 +695,17 @@ def create_cycle_issue_activity( issue_activities.append( IssueActivity( issue_id=updated_record.get("issue_id"), - actor=actor, + actor_id=actor_id, verb="updated", old_value=old_cycle.name, new_value=new_cycle.name, field="cycles", - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}", old_identifier=old_cycle.id, new_identifier=new_cycle.id, + epoch=epoch, ) ) @@ -785,21 +717,29 @@ def create_cycle_issue_activity( issue_activities.append( IssueActivity( issue_id=created_record.get("fields").get("issue"), - actor=actor, + actor_id=actor_id, verb="created", old_value="", new_value=cycle.name, field="cycles", - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"added cycle {cycle.name}", new_identifier=cycle.id, + epoch=epoch, ) ) def delete_cycle_issue_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( @@ -814,21 +754,29 @@ def delete_cycle_issue_activity( issue_activities.append( IssueActivity( issue_id=issue, - actor=actor, + actor_id=actor_id, verb="deleted", old_value=cycle.name if cycle is not None else "", new_value="", field="cycles", - project=project, - workspace=project.workspace, + 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, + epoch=epoch, ) ) def create_module_issue_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( @@ -850,16 +798,17 @@ def create_module_issue_activity( issue_activities.append( IssueActivity( issue_id=updated_record.get("issue_id"), - actor=actor, + actor_id=actor_id, verb="updated", old_value=old_module.name, new_value=new_module.name, field="modules", - project=project, - workspace=project.workspace, - comment=f"updated module from {old_module.name} to {new_module.name}", + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated module to ", old_identifier=old_module.id, new_identifier=new_module.id, + epoch=epoch, ) ) @@ -870,21 +819,29 @@ def create_module_issue_activity( issue_activities.append( IssueActivity( issue_id=created_record.get("fields").get("issue"), - actor=actor, + actor_id=actor_id, verb="created", old_value="", new_value=module.name, field="modules", - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"added module {module.name}", new_identifier=module.id, + epoch=epoch, ) ) def delete_module_issue_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( @@ -899,21 +856,29 @@ def delete_module_issue_activity( issue_activities.append( IssueActivity( issue_id=issue, - actor=actor, + actor_id=actor_id, verb="deleted", old_value=module.name if module is not None else "", new_value="", field="modules", - project=project, - workspace=project.workspace, - comment=f"removed this issue from {module.name if module is not None else None}", + 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, + epoch=epoch, ) ) def create_link_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + actor_id, + workspace_id, + issue_activities, + epoch, ): requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( @@ -923,20 +888,28 @@ def create_link_activity( issue_activities.append( IssueActivity( issue_id=issue_id, - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"created a link", verb="created", - actor=actor, + actor_id=actor_id, field="link", new_value=requested_data.get("url", ""), new_identifier=requested_data.get("id", None), + epoch=epoch, ) ) def update_link_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( @@ -947,24 +920,31 @@ def update_link_activity( issue_activities.append( IssueActivity( issue_id=issue_id, - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"updated a link", verb="updated", - actor=actor, + actor_id=actor_id, field="link", old_value=current_instance.get("url", ""), old_identifier=current_instance.get("id"), new_value=requested_data.get("url", ""), new_identifier=current_instance.get("id", None), + epoch=epoch, ) ) def delete_link_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): - current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -972,20 +952,28 @@ def delete_link_activity( issue_activities.append( IssueActivity( issue_id=issue_id, - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"deleted the link", verb="deleted", - actor=actor, + actor_id=actor_id, field="link", old_value=current_instance.get("url", ""), - new_value="" + new_value="", + epoch=epoch, ) ) def create_attachment_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + actor_id, + workspace_id, + issue_activities, + epoch, ): requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( @@ -995,30 +983,455 @@ def create_attachment_activity( issue_activities.append( IssueActivity( issue_id=issue_id, - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"created an attachment", verb="created", - actor=actor, + actor_id=actor_id, field="attachment", new_value=current_instance.get("asset", ""), new_identifier=current_instance.get("id", None), + epoch=epoch, ) ) def delete_attachment_activity( - requested_data, current_instance, issue_id, project, actor, issue_activities + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, ): issue_activities.append( IssueActivity( issue_id=issue_id, - project=project, - workspace=project.workspace, + project_id=project_id, + workspace_id=workspace_id, comment=f"deleted the attachment", verb="deleted", - actor=actor, + actor_id=actor_id, field="attachment", + epoch=epoch, + ) + ) + + +def create_issue_reaction_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + requested_data = json.loads(requested_data) if requested_data is not None else None + if requested_data and requested_data.get("reaction") is not None: + issue_reaction = ( + IssueReaction.objects.filter( + reaction=requested_data.get("reaction"), + project_id=project_id, + actor_id=actor_id, + ) + .values_list("id", flat=True) + .first() + ) + if issue_reaction is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="created", + old_value=None, + new_value=requested_data.get("reaction"), + field="reaction", + project_id=project_id, + workspace_id=workspace_id, + comment="added the reaction", + old_identifier=None, + new_identifier=issue_reaction, + epoch=epoch, + ) + ) + + +def delete_issue_reaction_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance and current_instance.get("reaction") is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="deleted", + old_value=current_instance.get("reaction"), + new_value=None, + field="reaction", + project_id=project_id, + workspace_id=workspace_id, + comment="removed the reaction", + old_identifier=current_instance.get("identifier"), + new_identifier=None, + epoch=epoch, + ) + ) + + +def create_comment_reaction_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + requested_data = json.loads(requested_data) if requested_data is not None else None + if requested_data and requested_data.get("reaction") is not None: + comment_reaction_id, comment_id = ( + CommentReaction.objects.filter( + reaction=requested_data.get("reaction"), + project_id=project_id, + actor_id=actor_id, + ) + .values_list("id", "comment__id") + .first() + ) + comment = IssueComment.objects.get(pk=comment_id, project_id=project_id) + if ( + comment is not None + and comment_reaction_id is not None + and comment_id is not None + ): + issue_activities.append( + IssueActivity( + issue_id=comment.issue_id, + actor_id=actor_id, + verb="created", + old_value=None, + new_value=requested_data.get("reaction"), + field="reaction", + project_id=project_id, + workspace_id=workspace_id, + comment="added the reaction", + old_identifier=None, + new_identifier=comment_reaction_id, + epoch=epoch, + ) + ) + + +def delete_comment_reaction_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance and current_instance.get("reaction") is not None: + issue_id = ( + IssueComment.objects.filter( + pk=current_instance.get("comment_id"), project_id=project_id + ) + .values_list("issue_id", flat=True) + .first() + ) + if issue_id is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="deleted", + old_value=current_instance.get("reaction"), + new_value=None, + field="reaction", + project_id=project_id, + workspace_id=workspace_id, + comment="removed the reaction", + old_identifier=current_instance.get("identifier"), + new_identifier=None, + epoch=epoch, + ) + ) + + +def create_issue_vote_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + requested_data = json.loads(requested_data) if requested_data is not None else None + if requested_data and requested_data.get("vote") is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="created", + old_value=None, + new_value=requested_data.get("vote"), + field="vote", + project_id=project_id, + workspace_id=workspace_id, + comment="added the vote", + old_identifier=None, + new_identifier=None, + epoch=epoch, + ) + ) + + +def delete_issue_vote_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance and current_instance.get("vote") is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="deleted", + old_value=current_instance.get("vote"), + new_value=None, + field="vote", + project_id=project_id, + workspace_id=workspace_id, + comment="removed the vote", + old_identifier=current_instance.get("identifier"), + new_identifier=None, + epoch=epoch, + ) + ) + + +def create_issue_relation_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + requested_data = json.loads(requested_data) if requested_data is not None else None + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance is None and requested_data.get("related_list") is not None: + for issue_relation in requested_data.get("related_list"): + if issue_relation.get("relation_type") == "blocked_by": + relation_type = "blocking" + else: + relation_type = issue_relation.get("relation_type") + issue = Issue.objects.get(pk=issue_relation.get("issue")) + issue_activities.append( + IssueActivity( + issue_id=issue_relation.get("related_issue"), + actor_id=actor_id, + verb="created", + old_value="", + new_value=f"{issue.project.identifier}-{issue.sequence_id}", + field=relation_type, + project_id=project_id, + workspace_id=workspace_id, + comment=f"added {relation_type} relation", + old_identifier=issue_relation.get("issue"), + ) + ) + issue = Issue.objects.get(pk=issue_relation.get("related_issue")) + issue_activities.append( + IssueActivity( + issue_id=issue_relation.get("issue"), + actor_id=actor_id, + verb="created", + old_value="", + new_value=f"{issue.project.identifier}-{issue.sequence_id}", + field=f'{issue_relation.get("relation_type")}', + project_id=project_id, + workspace_id=workspace_id, + comment=f'added {issue_relation.get("relation_type")} relation', + old_identifier=issue_relation.get("related_issue"), + epoch=epoch, + ) + ) + + +def delete_issue_relation_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + requested_data = json.loads(requested_data) if requested_data is not None else None + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance is not None and requested_data.get("related_list") is None: + if current_instance.get("relation_type") == "blocked_by": + relation_type = "blocking" + else: + relation_type = current_instance.get("relation_type") + issue = Issue.objects.get(pk=current_instance.get("issue")) + issue_activities.append( + IssueActivity( + issue_id=current_instance.get("related_issue"), + actor_id=actor_id, + verb="deleted", + old_value=f"{issue.project.identifier}-{issue.sequence_id}", + new_value="", + field=relation_type, + project_id=project_id, + workspace_id=workspace_id, + comment=f"deleted {relation_type} relation", + old_identifier=current_instance.get("issue"), + epoch=epoch, + ) + ) + issue = Issue.objects.get(pk=current_instance.get("related_issue")) + issue_activities.append( + IssueActivity( + issue_id=current_instance.get("issue"), + actor_id=actor_id, + verb="deleted", + old_value=f"{issue.project.identifier}-{issue.sequence_id}", + new_value="", + field=f'{current_instance.get("relation_type")}', + project_id=project_id, + workspace_id=workspace_id, + comment=f'deleted {current_instance.get("relation_type")} relation', + old_identifier=current_instance.get("related_issue"), + epoch=epoch, + ) + ) + + +def create_draft_issue_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project_id=project_id, + workspace_id=workspace_id, + comment=f"drafted the issue", + field="draft", + verb="created", + actor_id=actor_id, + epoch=epoch, + ) + ) + + +def update_draft_issue_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + requested_data = json.loads(requested_data) if requested_data is not None else None + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if ( + requested_data.get("is_draft") is not None + and requested_data.get("is_draft") == False + ): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project_id=project_id, + workspace_id=workspace_id, + comment=f"created the issue", + verb="updated", + actor_id=actor_id, + epoch=epoch, + ) + ) + else: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project_id=project_id, + workspace_id=workspace_id, + comment=f"updated the draft issue", + field="draft", + verb="updated", + actor_id=actor_id, + epoch=epoch, + ) + ) + + +def delete_draft_issue_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + issue_activities.append( + IssueActivity( + project_id=project_id, + workspace_id=workspace_id, + comment=f"deleted the draft issue", + field="draft", + verb="deleted", + actor_id=actor_id, + epoch=epoch, ) ) @@ -1032,37 +1445,22 @@ def issue_activity( issue_id, actor_id, project_id, + epoch, subscriber=True, ): try: issue_activities = [] - actor = User.objects.get(pk=actor_id) project = Project.objects.get(pk=project_id) + issue = Issue.objects.filter(pk=issue_id).first() + workspace_id = project.workspace_id - if type not in [ - "cycle.activity.created", - "cycle.activity.deleted", - "module.activity.created", - "module.activity.deleted", - ]: - issue = Issue.objects.filter(pk=issue_id).first() - - if issue is not None: - try: - issue.updated_at = timezone.now() - issue.save(update_fields=["updated_at"]) - except Exception as e: - pass - - if subscriber: - # add the user to issue subscriber - try: - _ = IssueSubscriber.objects.get_or_create( - issue_id=issue_id, subscriber=actor - ) - except Exception as e: - pass + if issue is not None: + 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, @@ -1080,17 +1478,30 @@ def issue_activity( "link.activity.deleted": delete_link_activity, "attachment.activity.created": create_attachment_activity, "attachment.activity.deleted": delete_attachment_activity, + "issue_relation.activity.created": create_issue_relation_activity, + "issue_relation.activity.deleted": delete_issue_relation_activity, + "issue_reaction.activity.created": create_issue_reaction_activity, + "issue_reaction.activity.deleted": delete_issue_reaction_activity, + "comment_reaction.activity.created": create_comment_reaction_activity, + "comment_reaction.activity.deleted": delete_comment_reaction_activity, + "issue_vote.activity.created": create_issue_vote_activity, + "issue_vote.activity.deleted": delete_issue_vote_activity, + "issue_draft.activity.created": create_draft_issue_activity, + "issue_draft.activity.updated": update_draft_issue_activity, + "issue_draft.activity.deleted": delete_draft_issue_activity, } func = ACTIVITY_MAPPER.get(type) if func is not None: func( - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, + requested_data=requested_data, + current_instance=current_instance, + issue_id=issue_id, + project_id=project_id, + workspace_id=workspace_id, + actor_id=actor_id, + issue_activities=issue_activities, + epoch=epoch, ) # Save all the values to database @@ -1114,80 +1525,19 @@ def issue_activity( except Exception as e: capture_exception(e) - if type not in [ - "cycle.activity.created", - "cycle.activity.deleted", - "module.activity.created", - "module.activity.deleted", - ]: - # Create Notifications - bulk_notifications = [] - - issue_subscribers = list( - IssueSubscriber.objects.filter(project=project, issue_id=issue_id) - .exclude(subscriber_id=actor_id) - .values_list("subscriber", flat=True) - ) - - issue_assignees = list( - IssueAssignee.objects.filter(project=project, issue_id=issue_id) - .exclude(assignee_id=actor_id) - .values_list("assignee", flat=True) - ) - - issue_subscribers = issue_subscribers + issue_assignees - - issue = Issue.objects.filter(pk=issue_id).first() - - # Add bot filtering - if ( - issue is not None - and issue.created_by_id is not None - and not issue.created_by.is_bot - and str(issue.created_by_id) != str(actor_id) - ): - issue_subscribers = issue_subscribers + [issue.created_by_id] - - for subscriber in issue_subscribers: - for issue_activity in issue_activities_created: - bulk_notifications.append( - Notification( - workspace=project.workspace, - sender="in_app:issue_activities", - triggered_by_id=actor_id, - receiver_id=subscriber, - entity_identifier=issue_id, - entity_name="issue", - project=project, - title=issue_activity.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(issue_activity.id), - "verb": str(issue_activity.verb), - "field": str(issue_activity.field), - "actor": str(issue_activity.actor_id), - "new_value": str(issue_activity.new_value), - "old_value": str(issue_activity.old_value), - "issue_comment": str( - issue_activity.issue_comment.comment_stripped - if issue_activity.issue_comment is not None - else "" - ), - }, - }, - ) - ) - - # Bulk create notifications - Notification.objects.bulk_create(bulk_notifications, batch_size=100) + notifications.delay( + type=type, + issue_id=issue_id, + actor_id=actor_id, + project_id=project_id, + subscriber=subscriber, + issue_activities_created=json.dumps( + IssueActivitySerializer(issue_activities_created, many=True).data, + cls=DjangoJSONEncoder, + ), + requested_data=requested_data, + current_instance=current_instance, + ) return except Exception as e: diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index a1f4a3e920e..4d77eb1246e 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -32,7 +32,7 @@ def archive_old_issues(): archive_in = project.archive_in # Get all the issues whose updated_at in less that the archive_in month - issues = Issue.objects.filter( + issues = Issue.issue_objects.filter( Q( project=project_id, archived_at__isnull=True, @@ -58,27 +58,32 @@ def archive_old_issues(): # Check if Issues if issues: + # Set the archive time to current time + archive_at = timezone.now().date() + issues_to_update = [] for issue in issues: - issue.archived_at = timezone.now() + issue.archived_at = archive_at issues_to_update.append(issue) # Bulk Update the issues and log the activity - updated_issues = Issue.objects.bulk_update( - issues_to_update, ["archived_at"], batch_size=100 - ) - [ - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"archived_at": str(issue.archived_at)}), - actor_id=str(project.created_by_id), - issue_id=issue.id, - project_id=project_id, - current_instance=None, - subscriber=False, + if issues_to_update: + Issue.objects.bulk_update( + issues_to_update, ["archived_at"], batch_size=100 ) - for issue in updated_issues - ] + _ = [ + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"archived_at": str(archive_at)}), + actor_id=str(project.created_by_id), + issue_id=issue.id, + project_id=project_id, + current_instance=json.dumps({"archived_at": None}), + subscriber=False, + epoch=int(timezone.now().timestamp()) + ) + for issue in issues_to_update + ] return except Exception as e: if settings.DEBUG: @@ -99,7 +104,7 @@ def close_old_issues(): close_in = project.close_in # Get all the issues whose updated_at in less that the close_in month - issues = Issue.objects.filter( + issues = Issue.issue_objects.filter( Q( project=project_id, archived_at__isnull=True, @@ -136,19 +141,21 @@ def close_old_issues(): issues_to_update.append(issue) # Bulk Update the issues and log the activity - updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) - [ - issue_activity.delay( - type="issue.activity.updated", - requested_data=json.dumps({"closed_to": str(issue.state_id)}), - actor_id=str(project.created_by_id), - issue_id=issue.id, - project_id=project_id, - current_instance=None, - subscriber=False, - ) - for issue in updated_issues - ] + if issues_to_update: + Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) + [ + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"closed_to": str(issue.state_id)}), + actor_id=str(project.created_by_id), + issue_id=issue.id, + project_id=project_id, + current_instance=None, + subscriber=False, + epoch=int(timezone.now().timestamp()) + ) + for issue in issues_to_update + ] return except Exception as e: if settings.DEBUG: diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index 91cc461bb20..71f6db8da84 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -17,7 +17,7 @@ def magic_link(email, key, token, current_site): from_email_string = settings.EMAIL_FROM - subject = f"Login for Plane" + subject = "Login for Plane" context = {"magic_url": abs_url, "code": token} diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py new file mode 100644 index 00000000000..4380f4ee90e --- /dev/null +++ b/apiserver/plane/bgtasks/notification_task.py @@ -0,0 +1,274 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone + +# Module imports +from plane.db.models import ( + IssueMention, + IssueSubscriber, + Project, + User, + IssueAssignee, + Issue, + Notification, + IssueComment, +) + +# Third Party imports +from celery import shared_task +from bs4 import BeautifulSoup + + +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 + new_mentions = [ + mention for mention in mentions_newer if mention not in mentions_older] + + return new_mentions + +# Get Removed Mention + + +def get_removed_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 + removed_mentions = [ + mention for mention in mentions_older if mention not in mentions_newer] + + return removed_mentions + +# Adds mentions as subscribers + + +def extract_mentions_as_subscribers(project_id, issue_id, mentions): + # mentions is an array of User IDs representing the FILTERED set of mentioned users + + bulk_mention_subscribers = [] + + for mention_id in 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, + ).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, + )) + 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. + mentions = [] + # Convert string to dictionary + data = json.loads(issue_instance) + html = data.get("description_html") + soup = BeautifulSoup(html, 'html.parser') + mention_tags = soup.find_all( + 'mention-component', attrs={'target': 'users'}) + + mentions = [mention_tag['id'] for mention_tag in mention_tags] + + return list(set(mentions)) + except Exception as e: + return [] + + +@shared_task +def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance): + issue_activities_created = ( + json.loads( + issue_activities_created) if issue_activities_created is not None else None + ) + if type not in [ + "cycle.activity.created", + "cycle.activity.deleted", + "module.activity.created", + "module.activity.deleted", + "issue_reaction.activity.created", + "issue_reaction.activity.deleted", + "comment_reaction.activity.created", + "comment_reaction.activity.deleted", + "issue_vote.activity.created", + "issue_vote.activity.deleted", + "issue_draft.activity.created", + "issue_draft.activity.updated", + "issue_draft.activity.deleted", + ]: + # Create Notifications + 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 + """ + + # 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) + + # 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) + ) + + issue_assignees = list( + IssueAssignee.objects.filter( + project_id=project_id, issue_id=issue_id) + .exclude(assignee_id=actor_id) + .values_list("assignee", flat=True) + ) + + issue_subscribers = issue_subscribers + issue_assignees + + issue = Issue.objects.filter(pk=issue_id).first() + + if subscriber: + # add the user to issue subscriber + try: + _ = IssueSubscriber.objects.get_or_create( + 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)): + 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) + bulk_notifications.append( + Notification( + workspace=project.workspace, + sender="in_app:issue_activities", + triggered_by_id=actor_id, + receiver_id=subscriber, + entity_identifier=issue_id, + entity_name="issue", + project=project, + title=issue_activity.get("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(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")), + "issue_comment": str( + issue_comment.comment_stripped + if issue_activity.get("issue_comment") is not None + else "" + ), + }, + }, + ) + ) + + # Add Mentioned as Issue Subscribers + IssueSubscriber.objects.bulk_create( + mention_subscribers, batch_size=100) + + for mention_id in new_mentions: + if (mention_id != actor_id): + for issue_activity in issue_activities_created: + bulk_notifications.append( + Notification( + workspace=project.workspace, + sender="in_app:issue_activities:mention", + triggered_by_id=actor_id, + receiver_id=mention_id, + entity_identifier=issue_id, + entity_name="issue", + project=project, + message=f"You have been mentioned in the issue {issue.name}", + 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(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")), + }, + }, + ) + ) + + # 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 create notifications + Notification.objects.bulk_create(bulk_notifications, batch_size=100) diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index d84a0b414f6..94be6f879d1 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -11,7 +11,7 @@ from slack_sdk.errors import SlackApiError # Module imports -from plane.db.models import Workspace, User, WorkspaceMemberInvite +from plane.db.models import Workspace, WorkspaceMemberInvite @shared_task diff --git a/apiserver/plane/celery.py b/apiserver/plane/celery.py index 15fe8af52b9..dfb094339b1 100644 --- a/apiserver/plane/celery.py +++ b/apiserver/plane/celery.py @@ -29,4 +29,4 @@ # Load task modules from all registered Django app configs. app.autodiscover_tasks() -app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler' \ No newline at end of file +app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler' diff --git a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py index f7d6a979de1..01af46d20cf 100644 --- a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py +++ b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py @@ -3,7 +3,7 @@ from django.conf import settings from django.db import migrations, models import django.db.models.deletion - +import uuid def update_user_timezones(apps, schema_editor): UserModel = apps.get_model("db", "User") @@ -31,5 +31,30 @@ class Migration(migrations.Migration): name='title', field=models.CharField(blank=True, max_length=255, null=True), ), - migrations.RunPython(update_user_timezones) + migrations.RunPython(update_user_timezones), + migrations.AlterField( + model_name='issuevote', + name='vote', + field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1), + ), + migrations.CreateModel( + name='ProjectPublicMember', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='public_project_members', to=settings.AUTH_USER_MODEL)), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ], + options={ + 'verbose_name': 'Project Public Member', + 'verbose_name_plural': 'Project Public Members', + 'db_table': 'project_public_members', + 'ordering': ('-created_at',), + 'unique_together': {('project', 'member')}, + }, + ), ] diff --git a/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py b/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py deleted file mode 100644 index d8063acc052..00000000000 --- a/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.2.3 on 2023-08-29 07:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('db', '0041_cycle_sort_order_issuecomment_access_and_more'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='issuevote', - unique_together=set(), - ), - migrations.AlterField( - model_name='issuevote', - name='vote', - field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1), - ), - migrations.AlterUniqueTogether( - name='issuevote', - unique_together={('issue', 'actor', 'vote')}, - ), - ] diff --git a/apiserver/plane/db/migrations/0043_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0043_alter_analyticview_created_by_and_more.py new file mode 100644 index 00000000000..5a806c7046a --- /dev/null +++ b/apiserver/plane/db/migrations/0043_alter_analyticview_created_by_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 4.2.3 on 2023-09-12 07:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +from plane.db.models import IssueRelation +from sentry_sdk import capture_exception +import uuid + + +def create_issue_relation(apps, schema_editor): + try: + IssueBlockerModel = apps.get_model("db", "IssueBlocker") + updated_issue_relation = [] + for blocked_issue in IssueBlockerModel.objects.all(): + updated_issue_relation.append( + IssueRelation( + issue_id=blocked_issue.block_id, + related_issue_id=blocked_issue.blocked_by_id, + relation_type="blocked_by", + project_id=blocked_issue.project_id, + workspace_id=blocked_issue.workspace_id, + created_by_id=blocked_issue.created_by_id, + updated_by_id=blocked_issue.updated_by_id, + ) + ) + IssueRelation.objects.bulk_create(updated_issue_relation, batch_size=100) + except Exception as e: + print(e) + capture_exception(e) + + +def update_issue_priority_choice(apps, schema_editor): + IssueModel = apps.get_model("db", "Issue") + updated_issues = [] + for obj in IssueModel.objects.filter(priority=None): + obj.priority = "none" + updated_issues.append(obj) + IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100) + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0042_alter_analyticview_created_by_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='IssueRelation', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('relation_type', models.CharField(choices=[('duplicate', 'Duplicate'), ('relates_to', 'Relates To'), ('blocked_by', 'Blocked By')], default='blocked_by', max_length=20, verbose_name='Issue Relation Type')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relation', to='db.issue')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), + ('related_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_related', to='db.issue')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ], + options={ + 'verbose_name': 'Issue Relation', + 'verbose_name_plural': 'Issue Relations', + 'db_table': 'issue_relations', + 'ordering': ('-created_at',), + 'unique_together': {('issue', 'related_issue')}, + }, + ), + migrations.AddField( + model_name='issue', + name='is_draft', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='issue', + name='priority', + field=models.CharField(choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low'), ('none', 'None')], default='none', max_length=30, verbose_name='Issue Priority'), + ), + migrations.RunPython(create_issue_relation), + migrations.RunPython(update_issue_priority_choice), + ] diff --git a/apiserver/plane/db/migrations/0044_auto_20230913_0709.py b/apiserver/plane/db/migrations/0044_auto_20230913_0709.py new file mode 100644 index 00000000000..19a1449af46 --- /dev/null +++ b/apiserver/plane/db/migrations/0044_auto_20230913_0709.py @@ -0,0 +1,138 @@ +# Generated by Django 4.2.3 on 2023-09-13 07:09 + +from django.db import migrations + + +def workspace_member_props(old_props): + new_props = { + "filters": { + "priority": old_props.get("filters", {}).get("priority", None), + "state": old_props.get("filters", {}).get("state", None), + "state_group": old_props.get("filters", {}).get("state_group", None), + "assignees": old_props.get("filters", {}).get("assignees", None), + "created_by": old_props.get("filters", {}).get("created_by", None), + "labels": old_props.get("filters", {}).get("labels", None), + "start_date": old_props.get("filters", {}).get("start_date", None), + "target_date": old_props.get("filters", {}).get("target_date", None), + "subscriber": old_props.get("filters", {}).get("subscriber", None), + }, + "display_filters": { + "group_by": old_props.get("groupByProperty", None), + "order_by": old_props.get("orderBy", "-created_at"), + "type": old_props.get("filters", {}).get("type", None), + "sub_issue": old_props.get("showSubIssues", True), + "show_empty_groups": old_props.get("showEmptyGroups", True), + "layout": old_props.get("issueView", "list"), + "calendar_date_range": old_props.get("calendarDateRange", ""), + }, + "display_properties": { + "assignee": old_props.get("properties", {}).get("assignee", True), + "attachment_count": old_props.get("properties", {}).get("attachment_count", True), + "created_on": old_props.get("properties", {}).get("created_on", True), + "due_date": old_props.get("properties", {}).get("due_date", True), + "estimate": old_props.get("properties", {}).get("estimate", True), + "key": old_props.get("properties", {}).get("key", True), + "labels": old_props.get("properties", {}).get("labels", True), + "link": old_props.get("properties", {}).get("link", True), + "priority": old_props.get("properties", {}).get("priority", True), + "start_date": old_props.get("properties", {}).get("start_date", True), + "state": old_props.get("properties", {}).get("state", True), + "sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True), + "updated_on": old_props.get("properties", {}).get("updated_on", True), + }, + } + return new_props + + +def project_member_props(old_props): + new_props = { + "filters": { + "priority": old_props.get("filters", {}).get("priority", None), + "state": old_props.get("filters", {}).get("state", None), + "state_group": old_props.get("filters", {}).get("state_group", None), + "assignees": old_props.get("filters", {}).get("assignees", None), + "created_by": old_props.get("filters", {}).get("created_by", None), + "labels": old_props.get("filters", {}).get("labels", None), + "start_date": old_props.get("filters", {}).get("start_date", None), + "target_date": old_props.get("filters", {}).get("target_date", None), + "subscriber": old_props.get("filters", {}).get("subscriber", None), + }, + "display_filters": { + "group_by": old_props.get("groupByProperty", None), + "order_by": old_props.get("orderBy", "-created_at"), + "type": old_props.get("filters", {}).get("type", None), + "sub_issue": old_props.get("showSubIssues", True), + "show_empty_groups": old_props.get("showEmptyGroups", True), + "layout": old_props.get("issueView", "list"), + "calendar_date_range": old_props.get("calendarDateRange", ""), + }, + } + return new_props + + +def cycle_module_props(old_props): + new_props = { + "filters": { + "priority": old_props.get("filters", {}).get("priority", None), + "state": old_props.get("filters", {}).get("state", None), + "state_group": old_props.get("filters", {}).get("state_group", None), + "assignees": old_props.get("filters", {}).get("assignees", None), + "created_by": old_props.get("filters", {}).get("created_by", None), + "labels": old_props.get("filters", {}).get("labels", None), + "start_date": old_props.get("filters", {}).get("start_date", None), + "target_date": old_props.get("filters", {}).get("target_date", None), + "subscriber": old_props.get("filters", {}).get("subscriber", None), + }, + } + return new_props + + +def update_workspace_member_view_props(apps, schema_editor): + WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember") + updated_workspace_member = [] + for obj in WorkspaceMemberModel.objects.all(): + obj.view_props = workspace_member_props(obj.view_props) + obj.default_props = workspace_member_props(obj.default_props) + updated_workspace_member.append(obj) + WorkspaceMemberModel.objects.bulk_update(updated_workspace_member, ["view_props", "default_props"], batch_size=100) + +def update_project_member_view_props(apps, schema_editor): + ProjectMemberModel = apps.get_model("db", "ProjectMember") + updated_project_member = [] + for obj in ProjectMemberModel.objects.all(): + obj.view_props = project_member_props(obj.view_props) + obj.default_props = project_member_props(obj.default_props) + updated_project_member.append(obj) + ProjectMemberModel.objects.bulk_update(updated_project_member, ["view_props", "default_props"], batch_size=100) + +def update_cycle_props(apps, schema_editor): + CycleModel = apps.get_model("db", "Cycle") + updated_cycle = [] + for obj in CycleModel.objects.all(): + if "filter" in obj.view_props: + obj.view_props = cycle_module_props(obj.view_props) + updated_cycle.append(obj) + CycleModel.objects.bulk_update(updated_cycle, ["view_props"], batch_size=100) + +def update_module_props(apps, schema_editor): + ModuleModel = apps.get_model("db", "Module") + updated_module = [] + for obj in ModuleModel.objects.all(): + if "filter" in obj.view_props: + obj.view_props = cycle_module_props(obj.view_props) + updated_module.append(obj) + ModuleModel.objects.bulk_update(updated_module, ["view_props"], batch_size=100) + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0043_alter_analyticview_created_by_and_more'), + ] + + operations = [ + migrations.RunPython(update_workspace_member_view_props), + migrations.RunPython(update_project_member_view_props), + migrations.RunPython(update_cycle_props), + migrations.RunPython(update_module_props), + ] diff --git a/apiserver/plane/db/migrations/0045_issueactivity_epoch_workspacemember_issue_props_and_more.py b/apiserver/plane/db/migrations/0045_issueactivity_epoch_workspacemember_issue_props_and_more.py new file mode 100644 index 00000000000..4b9c1b1eb94 --- /dev/null +++ b/apiserver/plane/db/migrations/0045_issueactivity_epoch_workspacemember_issue_props_and_more.py @@ -0,0 +1,79 @@ +# Generated by Django 4.2.5 on 2023-09-29 10:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import plane.db.models.workspace +import uuid + + +def update_issue_activity_priority(apps, schema_editor): + IssueActivity = apps.get_model("db", "IssueActivity") + updated_issue_activity = [] + for obj in IssueActivity.objects.filter(field="priority"): + # Set the old and new value to none if it is empty for Priority + obj.new_value = obj.new_value or "none" + obj.old_value = obj.old_value or "none" + updated_issue_activity.append(obj) + IssueActivity.objects.bulk_update( + updated_issue_activity, + ["new_value", "old_value"], + batch_size=2000, + ) + +def update_issue_activity_blocked(apps, schema_editor): + IssueActivity = apps.get_model("db", "IssueActivity") + updated_issue_activity = [] + for obj in IssueActivity.objects.filter(field="blocks"): + # Set the field to blocked_by + obj.field = "blocked_by" + updated_issue_activity.append(obj) + IssueActivity.objects.bulk_update( + updated_issue_activity, + ["field"], + batch_size=1000, + ) + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0044_auto_20230913_0709'), + ] + + operations = [ + migrations.CreateModel( + name='GlobalView', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=255, verbose_name='View Name')), + ('description', models.TextField(blank=True, verbose_name='View Description')), + ('query', models.JSONField(verbose_name='View Query')), + ('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)), + ('query_data', models.JSONField(default=dict)), + ('sort_order', models.FloatField(default=65535)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')), + ], + options={ + 'verbose_name': 'Global View', + 'verbose_name_plural': 'Global Views', + 'db_table': 'global_views', + 'ordering': ('-created_at',), + }, + ), + migrations.AddField( + model_name='workspacemember', + name='issue_props', + field=models.JSONField(default=plane.db.models.workspace.get_issue_props), + ), + migrations.AddField( + model_name='issueactivity', + name='epoch', + field=models.FloatField(null=True), + ), + migrations.RunPython(update_issue_activity_priority), + migrations.RunPython(update_issue_activity_blocked), + ] diff --git a/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py new file mode 100644 index 00000000000..ae5753e0710 --- /dev/null +++ b/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.5 on 2023-10-18 12:04 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import plane.db.models.issue +import uuid + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'), + ] + + operations = [ + migrations.CreateModel( + name="issue_mentions", + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4,editable=False, primary_key=True, serialize=False, unique=True)), + ('mention', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,related_name='issuemention_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to='db.issue')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuemention', to='db.project')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuemention_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issuemention', to='db.workspace')), + ], + options={ + 'verbose_name': 'IssueMention', + 'verbose_name_plural': 'IssueMentions', + 'db_table': 'issue_mentions', + 'ordering': ('-created_at',), + }, + ), + migrations.AlterField( + model_name='issueproperty', + name='properties', + field=models.JSONField(default=plane.db.models.issue.get_default_properties), + ), + ] \ No newline at end of file diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 90532dc64e3..d8286f8f8a1 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -27,11 +27,12 @@ IssueActivity, IssueProperty, IssueComment, - IssueBlocker, IssueLabel, IssueAssignee, Label, IssueBlocker, + IssueRelation, + IssueMention, IssueLink, IssueSequence, IssueAttachment, @@ -49,7 +50,7 @@ from .cycle import Cycle, CycleIssue, CycleFavorite -from .view import IssueView, IssueViewFavorite +from .view import GlobalView, IssueView, IssueViewFavorite from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite @@ -77,4 +78,4 @@ from .notification import Notification -from .exporter import ExporterHistory \ No newline at end of file +from .exporter import ExporterHistory diff --git a/apiserver/plane/db/models/exporter.py b/apiserver/plane/db/models/exporter.py index fce31c8e7d2..0383807b702 100644 --- a/apiserver/plane/db/models/exporter.py +++ b/apiserver/plane/db/models/exporter.py @@ -53,4 +53,4 @@ class Meta: def __str__(self): """Return name of the service""" - return f"{self.provider} <{self.workspace.name}>" \ No newline at end of file + return f"{self.provider} <{self.workspace.name}>" diff --git a/apiserver/plane/db/models/integration/__init__.py b/apiserver/plane/db/models/integration/__init__.py index 3f2be93b861..3bef687084a 100644 --- a/apiserver/plane/db/models/integration/__init__.py +++ b/apiserver/plane/db/models/integration/__init__.py @@ -1,3 +1,3 @@ from .base import Integration, WorkspaceIntegration from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync -from .slack import SlackProjectSync \ No newline at end of file +from .slack import SlackProjectSync diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index 130925c21d6..f4d152bb1b4 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -6,7 +6,6 @@ # Module imports from plane.db.models import ProjectBaseModel -from plane.db.mixins import AuditModel class GithubRepository(ProjectBaseModel): diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 1633cbaf917..0c227a158d5 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -16,6 +16,24 @@ from plane.utils.html_processor import strip_tags +def get_default_properties(): + return { + "assignee": True, + "start_date": True, + "due_date": True, + "labels": True, + "key": True, + "priority": True, + "state": True, + "sub_issue_count": True, + "link": True, + "attachment_count": True, + "estimate": True, + "created_on": True, + "updated_on": True, + } + + # TODO: Handle identifiers for Bulk Inserts - nk class IssueManager(models.Manager): def get_queryset(self): @@ -29,6 +47,7 @@ def get_queryset(self): | models.Q(issue_inbox__isnull=True) ) .exclude(archived_at__isnull=False) + .exclude(is_draft=True) ) @@ -38,6 +57,7 @@ class Issue(ProjectBaseModel): ("high", "High"), ("medium", "Medium"), ("low", "Low"), + ("none", "None"), ) parent = models.ForeignKey( "self", @@ -64,8 +84,7 @@ class Issue(ProjectBaseModel): max_length=30, choices=PRIORITY_CHOICES, verbose_name="Issue Priority", - null=True, - blank=True, + default="none", ) start_date = models.DateField(null=True, blank=True) target_date = models.DateField(null=True, blank=True) @@ -83,6 +102,7 @@ class Issue(ProjectBaseModel): sort_order = models.FloatField(default=65535) completed_at = models.DateTimeField(null=True) archived_at = models.DateField(null=True) + is_draft = models.BooleanField(default=False) objects = models.Manager() issue_objects = IssueManager() @@ -178,6 +198,56 @@ def __str__(self): return f"{self.block.name} {self.blocked_by.name}" +class IssueRelation(ProjectBaseModel): + RELATION_CHOICES = ( + ("duplicate", "Duplicate"), + ("relates_to", "Relates To"), + ("blocked_by", "Blocked By"), + ) + + issue = models.ForeignKey( + Issue, related_name="issue_relation", on_delete=models.CASCADE + ) + related_issue = models.ForeignKey( + Issue, related_name="issue_related", on_delete=models.CASCADE + ) + relation_type = models.CharField( + max_length=20, + choices=RELATION_CHOICES, + verbose_name="Issue Relation Type", + default="blocked_by", + ) + + class Meta: + unique_together = ["issue", "related_issue"] + verbose_name = "Issue Relation" + verbose_name_plural = "Issue Relations" + db_table = "issue_relations" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.issue.name} {self.related_issue.name}" + +class IssueMention(ProjectBaseModel): + issue = models.ForeignKey( + Issue, on_delete=models.CASCADE, related_name="issue_mention" + ) + mention = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="issue_mention", + ) + class Meta: + unique_together = ["issue", "mention"] + verbose_name = "Issue Mention" + verbose_name_plural = "Issue Mentions" + db_table = "issue_mentions" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.issue.name} {self.mention.email}" + + class IssueAssignee(ProjectBaseModel): issue = models.ForeignKey( Issue, on_delete=models.CASCADE, related_name="issue_assignee" @@ -276,6 +346,7 @@ class IssueActivity(ProjectBaseModel): ) old_identifier = models.UUIDField(null=True) new_identifier = models.UUIDField(null=True) + epoch = models.FloatField(null=True) class Meta: verbose_name = "Issue Activity" @@ -293,7 +364,9 @@ class IssueComment(ProjectBaseModel): comment_json = models.JSONField(blank=True, default=dict) comment_html = models.TextField(blank=True, default="

") attachments = ArrayField(models.URLField(), size=10, blank=True, default=list) - issue = models.ForeignKey(Issue, on_delete=models.CASCADE) + issue = models.ForeignKey( + Issue, on_delete=models.CASCADE, related_name="issue_comments" + ) # System can also create comment actor = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -333,7 +406,7 @@ class IssueProperty(ProjectBaseModel): on_delete=models.CASCADE, related_name="issue_property_user", ) - properties = models.JSONField(default=dict) + properties = models.JSONField(default=get_default_properties) class Meta: verbose_name = "Issue Property" @@ -481,7 +554,10 @@ class IssueVote(ProjectBaseModel): ) class Meta: - unique_together = ["issue", "actor", "vote"] + unique_together = [ + "issue", + "actor", + ] verbose_name = "Issue Vote" verbose_name_plural = "Issue Votes" db_table = "issue_votes" diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index da155af4029..f4ace65e5a0 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -4,9 +4,6 @@ # Django imports from django.db import models from django.conf import settings -from django.template.defaultfilters import slugify -from django.db.models.signals import post_save -from django.dispatch import receiver from django.core.validators import MinValueValidator, MaxValueValidator # Modeule imports @@ -25,13 +22,26 @@ def get_default_props(): return { - "filters": {"type": None}, - "orderBy": "-created_at", - "collapsed": True, - "issueView": "list", - "filterIssue": None, - "groupByProperty": None, - "showEmptyGroups": True, + "filters": { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + }, + "display_filters": { + "group_by": None, + "order_by": '-created_at', + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + }, } diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index 6a968af5345..44bc994d0c8 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -3,7 +3,41 @@ from django.conf import settings # Module import -from . import ProjectBaseModel +from . import ProjectBaseModel, BaseModel + + +class GlobalView(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", on_delete=models.CASCADE, related_name="global_views" + ) + name = models.CharField(max_length=255, verbose_name="View Name") + description = models.TextField(verbose_name="View Description", blank=True) + query = models.JSONField(verbose_name="View Query") + access = models.PositiveSmallIntegerField( + default=1, choices=((0, "Private"), (1, "Public")) + ) + query_data = models.JSONField(default=dict) + sort_order = models.FloatField(default=65535) + + class Meta: + verbose_name = "Global View" + verbose_name_plural = "Global Views" + db_table = "global_views" + ordering = ("-created_at",) + + def save(self, *args, **kwargs): + if self._state.adding: + largest_sort_order = GlobalView.objects.filter( + workspace=self.workspace + ).aggregate(largest=models.Max("sort_order"))["largest"] + if largest_sort_order is not None: + self.sort_order = largest_sort_order + 10000 + + super(GlobalView, self).save(*args, **kwargs) + + def __str__(self): + """Return name of the View""" + return f"{self.name} <{self.workspace.name}>" class IssueView(ProjectBaseModel): diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index 48d8c9f2d03..d1012f54914 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -16,26 +16,50 @@ def get_default_props(): return { - "filters": {"type": None}, - "groupByProperty": None, - "issueView": "list", - "orderBy": "-created_at", - "properties": { + "filters": { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + }, + "display_filters": { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + }, + "display_properties": { "assignee": True, + "attachment_count": True, + "created_on": True, "due_date": True, + "estimate": True, "key": True, "labels": True, + "link": True, "priority": True, + "start_date": True, "state": True, "sub_issue_count": True, - "attachment_count": True, - "link": True, - "estimate": True, - "created_on": True, "updated_on": True, - "start_date": True, - }, - "showEmptyGroups": True, + } + } + + +def get_issue_props(): + return { + "subscribed": True, + "assigned": True, + "created": True, + "all_issues": True, } @@ -74,6 +98,7 @@ class WorkspaceMember(BaseModel): company_role = models.TextField(null=True, blank=True) view_props = models.JSONField(default=get_default_props) default_props = models.JSONField(default=get_default_props) + issue_props = models.JSONField(default=get_issue_props) class Meta: unique_together = ["workspace", "member"] diff --git a/apiserver/plane/middleware/user_middleware.py b/apiserver/plane/middleware/user_middleware.py deleted file mode 100644 index 60dee9b731e..00000000000 --- a/apiserver/plane/middleware/user_middleware.py +++ /dev/null @@ -1,33 +0,0 @@ -import jwt -import pytz -from django.conf import settings -from django.utils import timezone -from plane.db.models import User - - -class UserMiddleware(object): - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - - try: - if request.headers.get("Authorization"): - authorization_header = request.headers.get("Authorization") - access_token = authorization_header.split(" ")[1] - decoded = jwt.decode( - access_token, settings.SECRET_KEY, algorithms=["HS256"] - ) - id = decoded['user_id'] - user = User.objects.get(id=id) - user.last_active = timezone.now() - user.token_updated_at = None - user.save() - timezone.activate(pytz.timezone(user.user_timezone)) - except Exception as e: - print(e) - - response = self.get_response(request) - - return response diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 9d293c0191e..76586b0feb6 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -12,6 +12,10 @@ DEBUG = int(os.environ.get("DEBUG", 1)) == 1 +ALLOWED_HOSTS = [ + "*", +] + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" @@ -114,3 +118,6 @@ GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" + +# Unsplash Access key +UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY") diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index acc1f34fedd..541a0cfd4c6 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -1,33 +1,34 @@ """Production settings and globals.""" -from urllib.parse import urlparse import ssl import certifi import dj_database_url -from urllib.parse import urlparse import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.redis import RedisIntegration +from urllib.parse import urlparse from .common import * # noqa # Database DEBUG = int(os.environ.get("DEBUG", 0)) == 1 -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": "plane", - "USER": os.environ.get("PGUSER", ""), - "PASSWORD": os.environ.get("PGPASSWORD", ""), - "HOST": os.environ.get("PGHOST", ""), +if bool(os.environ.get("DATABASE_URL")): + # Parse database configuration from $DATABASE_URL + DATABASES["default"] = dj_database_url.config() +else: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("POSTGRES_DB"), + "USER": os.environ.get("POSTGRES_USER"), + "PASSWORD": os.environ.get("POSTGRES_PASSWORD"), + "HOST": os.environ.get("POSTGRES_HOST"), + } } -} -# Parse database configuration from $DATABASE_URL -DATABASES["default"] = dj_database_url.config() SITE_ID = 1 # Set the variable true if running in docker environment @@ -197,7 +198,6 @@ STORAGES["default"] = { "BACKEND": "django_s3_storage.storage.S3Storage", } - # AWS Settings End # Enable Connection Pooling (if desired) @@ -265,15 +265,18 @@ CELERY_BROKER_URL = REDIS_URL CELERY_RESULT_BACKEND = REDIS_URL else: - CELERY_RESULT_BACKEND = broker_url CELERY_BROKER_URL = broker_url + CELERY_RESULT_BACKEND = broker_url GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) - +# Enable or Disable signups ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" # Scout Settings SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False) SCOUT_KEY = os.environ.get("SCOUT_KEY", "") SCOUT_NAME = "Plane" + +# Unsplash Access key +UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY") diff --git a/apiserver/plane/settings/selfhosted.py b/apiserver/plane/settings/selfhosted.py new file mode 100644 index 00000000000..ee529a7c332 --- /dev/null +++ b/apiserver/plane/settings/selfhosted.py @@ -0,0 +1,129 @@ +"""Self hosted settings and globals.""" +from urllib.parse import urlparse + +import dj_database_url +from urllib.parse import urlparse + + +from .common import * # noqa + +# Database +DEBUG = int(os.environ.get("DEBUG", 0)) == 1 + +# Docker configurations +DOCKERIZED = 1 +USE_MINIO = 1 + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "plane", + "USER": os.environ.get("PGUSER", ""), + "PASSWORD": os.environ.get("PGPASSWORD", ""), + "HOST": os.environ.get("PGHOST", ""), + } +} + +# Parse database configuration from $DATABASE_URL +DATABASES["default"] = dj_database_url.config() +SITE_ID = 1 + +# File size limit +FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) + +CORS_ALLOW_METHODS = [ + "DELETE", + "GET", + "OPTIONS", + "PATCH", + "POST", + "PUT", +] + +CORS_ALLOW_HEADERS = [ + "accept", + "accept-encoding", + "authorization", + "content-type", + "dnt", + "origin", + "user-agent", + "x-csrftoken", + "x-requested-with", +] + +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_ALL_ORIGINS = True + +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + +INSTALLED_APPS += ("storages",) +STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"} +# The AWS access key to use. +AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") +# The AWS secret access key to use. +AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key") +# The name of the bucket to store files in. +AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") +# The full URL to the S3 endpoint. Leave blank to use the default region URL. +AWS_S3_ENDPOINT_URL = os.environ.get( + "AWS_S3_ENDPOINT_URL", "http://plane-minio:9000" +) +# Default permissions +AWS_DEFAULT_ACL = "public-read" +AWS_QUERYSTRING_AUTH = False +AWS_S3_FILE_OVERWRITE = False + +# Custom Domain settings +parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost")) +AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}" +AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:" + +# Honor the 'X-Forwarded-Proto' header for request.is_secure() +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +# Allow all host headers +ALLOWED_HOSTS = [ + "*", +] + +# Security settings +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +# Redis URL +REDIS_URL = os.environ.get("REDIS_URL") + +# Caches +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": REDIS_URL, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, + } +} + +# URL used for email redirects +WEB_URL = os.environ.get("WEB_URL", "http://localhost") + +# Celery settings +CELERY_BROKER_URL = REDIS_URL +CELERY_RESULT_BACKEND = REDIS_URL + +# Enable or Disable signups +ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" + +# Analytics +ANALYTICS_BASE_API = False + +# OPEN AI Settings +OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1") +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False) +GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo") + diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index 5e274f8f32e..fe473234312 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -4,7 +4,6 @@ import certifi import dj_database_url -from urllib.parse import urlparse import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration @@ -218,3 +217,7 @@ GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1" + + +# Unsplash Access key +UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY") diff --git a/apiserver/plane/tests/__init__.py b/apiserver/plane/tests/__init__.py index f77d5060c01..0a0e47b0b01 100644 --- a/apiserver/plane/tests/__init__.py +++ b/apiserver/plane/tests/__init__.py @@ -1 +1 @@ -from .api import * \ No newline at end of file +from .api import * diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index 2b83ef8cf25..90643749c79 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -2,16 +2,13 @@ """ -# from django.contrib import admin from django.urls import path, include, re_path from django.views.generic import TemplateView from django.conf import settings -# from django.conf.urls.static import static urlpatterns = [ - # path("admin/", admin.site.urls), path("", TemplateView.as_view(template_name="index.html")), path("api/", include("plane.api.urls")), path("", include("plane.web.urls")), diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index 033452e0dcd..be52bcce445 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -12,34 +12,47 @@ from plane.db.models import Issue -def build_graph_plot(queryset, x_axis, y_axis, segment=None): - - temp_axis = x_axis - +def annotate_with_monthly_dimension(queryset, field_name, attribute): + # Get the year and the months + year = ExtractYear(field_name) + month = ExtractMonth(field_name) + # Concat the year and month + dimension = Concat(year, Value("-"), month, output_field=CharField()) + # Annotate the dimension + return queryset.annotate(**{attribute: dimension}) + +def extract_axis(queryset, x_axis): + # Format the dimension when the axis is in date if x_axis in ["created_at", "start_date", "target_date", "completed_at"]: - year = ExtractYear(x_axis) - month = ExtractMonth(x_axis) - dimension = Concat(year, Value("-"), month, output_field=CharField()) - queryset = queryset.annotate(dimension=dimension) - x_axis = "dimension" + queryset = annotate_with_monthly_dimension(queryset, x_axis, "dimension") + return queryset, "dimension" else: - queryset = queryset.annotate(dimension=F(x_axis)) - x_axis = "dimension" + return queryset.annotate(dimension=F(x_axis)), "dimension" - if x_axis in ["created_at", "start_date", "target_date", "completed_at"]: - queryset = queryset.exclude(x_axis__is_null=True) +def sort_data(data, temp_axis): + # When the axis is in priority order by + if temp_axis == "priority": + order = ["low", "medium", "high", "urgent", "none"] + return {key: data[key] for key in order if key in data} + else: + return dict(sorted(data.items(), key=lambda x: (x[0] == "none", x[0]))) +def build_graph_plot(queryset, x_axis, y_axis, segment=None): + # temp x_axis + temp_axis = x_axis + # Extract the x_axis and queryset + queryset, x_axis = extract_axis(queryset, x_axis) + if x_axis == "dimension": + queryset = queryset.exclude(dimension__isnull=True) + + # if segment in ["created_at", "start_date", "target_date", "completed_at"]: - year = ExtractYear(segment) - month = ExtractMonth(segment) - dimension = Concat(year, Value("-"), month, output_field=CharField()) - queryset = queryset.annotate(segmented=dimension) + queryset = annotate_with_monthly_dimension(queryset, segment, "segmented") segment = "segmented" queryset = queryset.values(x_axis) - # Group queryset by x_axis field - + # Issue count if y_axis == "issue_count": queryset = queryset.annotate( is_null=Case( @@ -49,43 +62,25 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None): ), dimension_ex=Coalesce("dimension", Value("null")), ).values("dimension") - if segment: - queryset = queryset.annotate(segment=F(segment)).values( - "dimension", "segment" - ) - else: - queryset = queryset.values("dimension") - + queryset = queryset.annotate(segment=F(segment)) if segment else queryset + queryset = queryset.values("dimension", "segment") if segment else queryset.values("dimension") queryset = queryset.annotate(count=Count("*")).order_by("dimension") - if y_axis == "estimate": + # Estimate + else: queryset = queryset.annotate(estimate=Sum("estimate_point")).order_by(x_axis) - if segment: - queryset = queryset.annotate(segment=F(segment)).values( - "dimension", "segment", "estimate" - ) - else: - queryset = queryset.values("dimension", "estimate") + queryset = queryset.annotate(segment=F(segment)) if segment else queryset + queryset = queryset.values("dimension", "segment", "estimate") if segment else queryset.values("dimension", "estimate") result_values = list(queryset) - grouped_data = {} - for key, items in groupby(result_values, key=lambda x: x[str("dimension")]): - grouped_data[str(key)] = list(items) - - sorted_data = grouped_data - if temp_axis == "priority": - order = ["low", "medium", "high", "urgent", "None"] - sorted_data = {key: grouped_data[key] for key in order if key in grouped_data} - else: - sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "None", x[0]))) - return sorted_data + grouped_data = {str(key): list(items) for key, items in groupby(result_values, key=lambda x: x[str("dimension")])} + return sort_data(grouped_data, temp_axis) def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): # Total Issues in Cycle or Module total_issues = queryset.total_issues - if cycle_id: # Get all dates between the two dates date_range = [ @@ -96,7 +91,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): chart_data = {str(date): 0 for date in date_range} completed_issues_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( workspace__slug=slug, project_id=project_id, issue_cycle__cycle_id=cycle_id, @@ -107,7 +102,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): .values("date", "total_completed") .order_by("date") ) - + if module_id: # Get all dates between the two dates date_range = [ @@ -118,7 +113,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): chart_data = {str(date): 0 for date in date_range} completed_issues_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( workspace__slug=slug, project_id=project_id, issue_module__module_id=module_id, @@ -130,18 +125,15 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): .order_by("date") ) - for date in date_range: cumulative_pending_issues = total_issues total_completed = 0 total_completed = sum( - [ - item["total_completed"] - for item in completed_issues_distribution - if item["date"] is not None and item["date"] <= date - ] + item["total_completed"] + for item in completed_issues_distribution + if item["date"] is not None and item["date"] <= date ) cumulative_pending_issues -= total_completed chart_data[str(date)] = cumulative_pending_issues - return chart_data \ No newline at end of file + return chart_data diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index 535bf6ebab4..853874b3178 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -15,7 +15,7 @@ def resolve_keys(group_keys, value): return value -def group_results(results_data, group_by): +def group_results(results_data, group_by, sub_group_by=False): """group results data into certain group_by Args: @@ -25,38 +25,140 @@ def group_results(results_data, group_by): Returns: obj: grouped results """ - response_dict = dict() - - if group_by == "priority": - response_dict = { - "urgent": [], - "high": [], - "medium": [], - "low": [], - "None": [], - } - - for value in results_data: - group_attribute = resolve_keys(group_by, value) - if isinstance(group_attribute, list): - if len(group_attribute): - for attrib in group_attribute: - if str(attrib) in response_dict: - response_dict[str(attrib)].append(value) + if sub_group_by: + main_responsive_dict = dict() + + if sub_group_by == "priority": + main_responsive_dict = { + "urgent": {}, + "high": {}, + "medium": {}, + "low": {}, + "none": {}, + } + + for value in results_data: + main_group_attribute = resolve_keys(sub_group_by, value) + group_attribute = resolve_keys(group_by, value) + if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list): + if len(main_group_attribute): + for attrib in main_group_attribute: + if str(attrib) not in main_responsive_dict: + main_responsive_dict[str(attrib)] = {} + if str(group_attribute) in main_responsive_dict[str(attrib)]: + main_responsive_dict[str(attrib)][str(group_attribute)].append(value) + else: + main_responsive_dict[str(attrib)][str(group_attribute)] = [] + main_responsive_dict[str(attrib)][str(group_attribute)].append(value) + else: + if str(None) not in main_responsive_dict: + main_responsive_dict[str(None)] = {} + + if str(group_attribute) in main_responsive_dict[str(None)]: + main_responsive_dict[str(None)][str(group_attribute)].append(value) + else: + main_responsive_dict[str(None)][str(group_attribute)] = [] + main_responsive_dict[str(None)][str(group_attribute)].append(value) + + elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list): + if str(main_group_attribute) not in main_responsive_dict: + main_responsive_dict[str(main_group_attribute)] = {} + if len(group_attribute): + for attrib in group_attribute: + if str(attrib) in main_responsive_dict[str(main_group_attribute)]: + main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value) + else: + main_responsive_dict[str(main_group_attribute)][str(attrib)] = [] + main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value) + else: + if str(None) in main_responsive_dict[str(main_group_attribute)]: + main_responsive_dict[str(main_group_attribute)][str(None)].append(value) + else: + main_responsive_dict[str(main_group_attribute)][str(None)] = [] + main_responsive_dict[str(main_group_attribute)][str(None)].append(value) + + elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list): + if len(main_group_attribute): + for main_attrib in main_group_attribute: + if str(main_attrib) not in main_responsive_dict: + main_responsive_dict[str(main_attrib)] = {} + if len(group_attribute): + for attrib in group_attribute: + if str(attrib) in main_responsive_dict[str(main_attrib)]: + main_responsive_dict[str(main_attrib)][str(attrib)].append(value) + else: + main_responsive_dict[str(main_attrib)][str(attrib)] = [] + main_responsive_dict[str(main_attrib)][str(attrib)].append(value) + else: + if str(None) in main_responsive_dict[str(main_attrib)]: + main_responsive_dict[str(main_attrib)][str(None)].append(value) + else: + main_responsive_dict[str(main_attrib)][str(None)] = [] + main_responsive_dict[str(main_attrib)][str(None)].append(value) + else: + if str(None) not in main_responsive_dict: + main_responsive_dict[str(None)] = {} + if len(group_attribute): + for attrib in group_attribute: + if str(attrib) in main_responsive_dict[str(None)]: + main_responsive_dict[str(None)][str(attrib)].append(value) + else: + main_responsive_dict[str(None)][str(attrib)] = [] + main_responsive_dict[str(None)][str(attrib)].append(value) else: - response_dict[str(attrib)] = [] - response_dict[str(attrib)].append(value) + if str(None) in main_responsive_dict[str(None)]: + main_responsive_dict[str(None)][str(None)].append(value) + else: + main_responsive_dict[str(None)][str(None)] = [] + main_responsive_dict[str(None)][str(None)].append(value) else: - if str(None) in response_dict: - response_dict[str(None)].append(value) + main_group_attribute = resolve_keys(sub_group_by, value) + group_attribute = resolve_keys(group_by, value) + + if str(main_group_attribute) not in main_responsive_dict: + main_responsive_dict[str(main_group_attribute)] = {} + + if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]: + main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value) else: - response_dict[str(None)] = [] - response_dict[str(None)].append(value) - else: - if str(group_attribute) in response_dict: - response_dict[str(group_attribute)].append(value) + main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = [] + main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value) + + return main_responsive_dict + + else: + response_dict = {} + + if group_by == "priority": + response_dict = { + "urgent": [], + "high": [], + "medium": [], + "low": [], + "none": [], + } + + for value in results_data: + group_attribute = resolve_keys(group_by, value) + if isinstance(group_attribute, list): + if len(group_attribute): + for attrib in group_attribute: + if str(attrib) in response_dict: + response_dict[str(attrib)].append(value) + else: + response_dict[str(attrib)] = [] + response_dict[str(attrib)].append(value) + else: + if str(None) in response_dict: + response_dict[str(None)].append(value) + else: + response_dict[str(None)] = [] + response_dict[str(None)].append(value) else: - response_dict[str(group_attribute)] = [] - response_dict[str(group_attribute)].append(value) + if str(group_attribute) in response_dict: + response_dict[str(group_attribute)].append(value) + else: + response_dict[str(group_attribute)] = [] + response_dict[str(group_attribute)].append(value) - return response_dict + return response_dict diff --git a/apiserver/plane/utils/imports.py b/apiserver/plane/utils/imports.py index 1a0d2924ec4..5f9f1c98c56 100644 --- a/apiserver/plane/utils/imports.py +++ b/apiserver/plane/utils/imports.py @@ -17,4 +17,4 @@ def import_submodules(context, root_module, path): for k, v in six.iteritems(vars(module)): if not k.startswith('_'): context[k] = v - context[module_name] = module \ No newline at end of file + context[module_name] = module 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/apiserver/plane/utils/ip_address.py b/apiserver/plane/utils/ip_address.py index 29a2fa520c7..06ca4353d64 100644 --- a/apiserver/plane/utils/ip_address.py +++ b/apiserver/plane/utils/ip_address.py @@ -4,4 +4,4 @@ def get_client_ip(request): ip = x_forwarded_for.split(',')[0] else: ip = request.META.get('REMOTE_ADDR') - return ip \ No newline at end of file + return ip diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 34e1e820384..75437fbee45 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -1,106 +1,175 @@ -from django.utils.timezone import make_aware -from django.utils.dateparse import parse_datetime +import re +import uuid +from datetime import timedelta +from django.utils import timezone + + +# The date from pattern +pattern = re.compile(r"\d+_(weeks|months)$") + +# check the valid uuids +def filter_valid_uuids(uuid_list): + valid_uuids = [] + for uuid_str in uuid_list: + try: + uuid_obj = uuid.UUID(uuid_str) + valid_uuids.append(uuid_obj) + except ValueError: + # ignore the invalid uuids + pass + return valid_uuids + + +# Get the 2_weeks, 3_months +def string_date_filter(filter, duration, subsequent, term, date_filter, offset): + now = timezone.now().date() + if term == "months": + if subsequent == "after": + if offset == "fromnow": + filter[f"{date_filter}__gte"] = now + timedelta(days=duration * 30) + else: + filter[f"{date_filter}__gte"] = now - timedelta(days=duration * 30) + else: + if offset == "fromnow": + filter[f"{date_filter}__lte"] = now + timedelta(days=duration * 30) + else: + filter[f"{date_filter}__lte"] = now - timedelta(days=duration * 30) + if term == "weeks": + if subsequent == "after": + if offset == "fromnow": + filter[f"{date_filter}__gte"] = now + timedelta(weeks=duration) + else: + filter[f"{date_filter}__gte"] = now - timedelta(weeks=duration) + else: + if offset == "fromnow": + filter[f"{date_filter}__lte"] = now + timedelta(days=duration) + else: + filter[f"{date_filter}__lte"] = now - timedelta(days=duration) + + +def date_filter(filter, date_term, queries): + """ + Handle all date filters + """ + for query in queries: + date_query = query.split(";") + if len(date_query) >= 2: + match = pattern.match(date_query[0]) + if match: + if len(date_query) == 3: + digit, term = date_query[0].split("_") + string_date_filter( + filter=filter, + duration=int(digit), + subsequent=date_query[1], + term=term, + date_filter="created_at__date", + offset=date_query[2], + ) + else: + if "after" in date_query: + filter[f"{date_term}__gte"] = date_query[0] + else: + filter[f"{date_term}__lte"] = date_query[0] + def filter_state(params, filter, method): if method == "GET": - states = params.get("state").split(",") + states = [item for item in params.get("state").split(",") if item != 'null'] + states = filter_valid_uuids(states) if len(states) and "" not in states: filter["state__in"] = states else: - if params.get("state", None) and len(params.get("state")): + if params.get("state", None) and len(params.get("state")) and params.get("state") != 'null': filter["state__in"] = params.get("state") return filter def filter_state_group(params, filter, method): if method == "GET": - state_group = params.get("state_group").split(",") + state_group = [item for item in params.get("state_group").split(",") if item != 'null'] if len(state_group) and "" not in state_group: filter["state__group__in"] = state_group else: - if params.get("state_group", None) and len(params.get("state_group")): + if params.get("state_group", None) and len(params.get("state_group")) and params.get("state_group") != 'null': filter["state__group__in"] = params.get("state_group") return filter - def filter_estimate_point(params, filter, method): if method == "GET": - estimate_points = params.get("estimate_point").split(",") + estimate_points = [item for item in params.get("estimate_point").split(",") if item != 'null'] if len(estimate_points) and "" not in estimate_points: filter["estimate_point__in"] = estimate_points else: - if params.get("estimate_point", None) and len(params.get("estimate_point")): + if params.get("estimate_point", None) and len(params.get("estimate_point")) and params.get("estimate_point") != 'null': filter["estimate_point__in"] = params.get("estimate_point") return filter def filter_priority(params, filter, method): if method == "GET": - priorities = params.get("priority").split(",") + priorities = [item for item in params.get("priority").split(",") if item != 'null'] if len(priorities) and "" not in priorities: - if len(priorities) == 1 and "null" in priorities: - filter["priority__isnull"] = True - elif len(priorities) > 1 and "null" in priorities: - filter["priority__isnull"] = True - filter["priority__in"] = [p for p in priorities if p != "null"] - else: - filter["priority__in"] = [p for p in priorities if p != "null"] - - else: - if params.get("priority", None) and len(params.get("priority")): - priorities = params.get("priority") - if len(priorities) == 1 and "null" in priorities: - filter["priority__isnull"] = True - elif len(priorities) > 1 and "null" in priorities: - filter["priority__isnull"] = True - filter["priority__in"] = [p for p in priorities if p != "null"] - else: - filter["priority__in"] = [p for p in priorities if p != "null"] - + filter["priority__in"] = priorities return filter def filter_parent(params, filter, method): if method == "GET": - parents = params.get("parent").split(",") + parents = [item for item in params.get("parent").split(",") if item != 'null'] + parents = filter_valid_uuids(parents) if len(parents) and "" not in parents: filter["parent__in"] = parents else: - if params.get("parent", None) and len(params.get("parent")): + if params.get("parent", None) and len(params.get("parent")) and params.get("parent") != 'null': filter["parent__in"] = params.get("parent") return filter def filter_labels(params, filter, method): if method == "GET": - labels = params.get("labels").split(",") + labels = [item for item in params.get("labels").split(",") if item != 'null'] + labels = filter_valid_uuids(labels) if len(labels) and "" not in labels: filter["labels__in"] = labels else: - if params.get("labels", None) and len(params.get("labels")): + if params.get("labels", None) and len(params.get("labels")) and params.get("labels") != 'null': filter["labels__in"] = params.get("labels") return filter def filter_assignees(params, filter, method): if method == "GET": - assignees = params.get("assignees").split(",") + assignees = [item for item in params.get("assignees").split(",") if item != 'null'] + assignees = filter_valid_uuids(assignees) if len(assignees) and "" not in assignees: filter["assignees__in"] = assignees else: - if params.get("assignees", None) and len(params.get("assignees")): + if params.get("assignees", None) and len(params.get("assignees")) and params.get("assignees") != 'null': filter["assignees__in"] = params.get("assignees") return filter +def filter_mentions(params, filter, method): + if method == "GET": + mentions = [item for item in params.get("mentions").split(",") if item != 'null'] + mentions = filter_valid_uuids(mentions) + if len(mentions) and "" not in mentions: + filter["issue_mention__mention__id__in"] = mentions + else: + if params.get("mentions", None) and len(params.get("mentions")) and params.get("mentions") != 'null': + filter["issue_mention__mention__id__in"] = params.get("mentions") + return filter + def filter_created_by(params, filter, method): if method == "GET": - created_bys = params.get("created_by").split(",") + created_bys = [item for item in params.get("created_by").split(",") if item != 'null'] + created_bys = filter_valid_uuids(created_bys) if len(created_bys) and "" not in created_bys: filter["created_by__in"] = created_bys else: - if params.get("created_by", None) and len(params.get("created_by")): + if params.get("created_by", None) and len(params.get("created_by")) and params.get("created_by") != 'null': filter["created_by__in"] = params.get("created_by") return filter @@ -115,20 +184,10 @@ def filter_created_at(params, filter, method): if method == "GET": created_ats = params.get("created_at").split(",") if len(created_ats) and "" not in created_ats: - for query in created_ats: - created_at_query = query.split(";") - if len(created_at_query) == 2 and "after" in created_at_query: - filter["created_at__date__gte"] = created_at_query[0] - else: - filter["created_at__date__lte"] = created_at_query[0] + date_filter(filter=filter, date_term="created_at__date", queries=created_ats) else: if params.get("created_at", None) and len(params.get("created_at")): - for query in params.get("created_at"): - created_at_query = query.split(";") - if len(created_at_query) == 2 and "after" in created_at_query: - filter["created_at__date__gte"] = created_at_query[0] - else: - filter["created_at__date__lte"] = created_at_query[0] + date_filter(filter=filter, date_term="created_at__date", queries=params.get("created_at", [])) return filter @@ -136,20 +195,10 @@ def filter_updated_at(params, filter, method): if method == "GET": updated_ats = params.get("updated_at").split(",") if len(updated_ats) and "" not in updated_ats: - for query in updated_ats: - updated_at_query = query.split(";") - if len(updated_at_query) == 2 and "after" in updated_at_query: - filter["updated_at__date__gte"] = updated_at_query[0] - else: - filter["updated_at__date__lte"] = updated_at_query[0] + date_filter(filter=filter, date_term="created_at__date", queries=updated_ats) else: if params.get("updated_at", None) and len(params.get("updated_at")): - for query in params.get("updated_at"): - updated_at_query = query.split(";") - if len(updated_at_query) == 2 and "after" in updated_at_query: - filter["updated_at__date__gte"] = updated_at_query[0] - else: - filter["updated_at__date__lte"] = updated_at_query[0] + date_filter(filter=filter, date_term="created_at__date", queries=params.get("updated_at", [])) return filter @@ -157,20 +206,10 @@ def filter_start_date(params, filter, method): if method == "GET": start_dates = params.get("start_date").split(",") if len(start_dates) and "" not in start_dates: - for query in start_dates: - start_date_query = query.split(";") - if len(start_date_query) == 2 and "after" in start_date_query: - filter["start_date__gte"] = start_date_query[0] - else: - filter["start_date__lte"] = start_date_query[0] + date_filter(filter=filter, date_term="start_date", queries=start_dates) else: if params.get("start_date", None) and len(params.get("start_date")): - for query in params.get("start_date"): - start_date_query = query.split(";") - if len(start_date_query) == 2 and "after" in start_date_query: - filter["start_date__gte"] = start_date_query[0] - else: - filter["start_date__lte"] = start_date_query[0] + filter["start_date"] = params.get("start_date") return filter @@ -178,21 +217,10 @@ def filter_target_date(params, filter, method): if method == "GET": target_dates = params.get("target_date").split(",") if len(target_dates) and "" not in target_dates: - for query in target_dates: - target_date_query = query.split(";") - if len(target_date_query) == 2 and "after" in target_date_query: - filter["target_date__gt"] = target_date_query[0] - else: - filter["target_date__lt"] = target_date_query[0] + date_filter(filter=filter, date_term="target_date", queries=target_dates) else: if params.get("target_date", None) and len(params.get("target_date")): - for query in params.get("target_date"): - target_date_query = query.split(";") - if len(target_date_query) == 2 and "after" in target_date_query: - filter["target_date__gt"] = target_date_query[0] - else: - filter["target_date__lt"] = target_date_query[0] - + filter["target_date"] = params.get("target_date") return filter @@ -200,20 +228,10 @@ def filter_completed_at(params, filter, method): if method == "GET": completed_ats = params.get("completed_at").split(",") if len(completed_ats) and "" not in completed_ats: - for query in completed_ats: - completed_at_query = query.split(";") - if len(completed_at_query) == 2 and "after" in completed_at_query: - filter["completed_at__date__gte"] = completed_at_query[0] - else: - filter["completed_at__lte"] = completed_at_query[0] + date_filter(filter=filter, date_term="completed_at__date", queries=completed_ats) else: if params.get("completed_at", None) and len(params.get("completed_at")): - for query in params.get("completed_at"): - completed_at_query = query.split(";") - if len(completed_at_query) == 2 and "after" in completed_at_query: - filter["completed_at__date__gte"] = completed_at_query[0] - else: - filter["completed_at__lte"] = completed_at_query[0] + date_filter(filter=filter, date_term="completed_at__date", queries=params.get("completed_at", [])) return filter @@ -229,47 +247,49 @@ def filter_issue_state_type(params, filter, method): return filter - def filter_project(params, filter, method): if method == "GET": - projects = params.get("project").split(",") + projects = [item for item in params.get("project").split(",") if item != 'null'] + projects = filter_valid_uuids(projects) if len(projects) and "" not in projects: filter["project__in"] = projects else: - if params.get("project", None) and len(params.get("project")): + if params.get("project", None) and len(params.get("project")) and params.get("project") != 'null': filter["project__in"] = params.get("project") return filter def filter_cycle(params, filter, method): if method == "GET": - cycles = params.get("cycle").split(",") + cycles = [item for item in params.get("cycle").split(",") if item != 'null'] + cycles = filter_valid_uuids(cycles) if len(cycles) and "" not in cycles: filter["issue_cycle__cycle_id__in"] = cycles else: - if params.get("cycle", None) and len(params.get("cycle")): + if params.get("cycle", None) and len(params.get("cycle")) and params.get("cycle") != 'null': filter["issue_cycle__cycle_id__in"] = params.get("cycle") return filter def filter_module(params, filter, method): if method == "GET": - modules = params.get("module").split(",") + modules = [item for item in params.get("module").split(",") if item != 'null'] + modules = filter_valid_uuids(modules) if len(modules) and "" not in modules: filter["issue_module__module_id__in"] = modules else: - if params.get("module", None) and len(params.get("module")): + if params.get("module", None) and len(params.get("module")) and params.get("module") != 'null': filter["issue_module__module_id__in"] = params.get("module") return filter def filter_inbox_status(params, filter, method): if method == "GET": - status = params.get("inbox_status").split(",") + status = [item for item in params.get("inbox_status").split(",") if item != 'null'] if len(status) and "" not in status: filter["issue_inbox__status__in"] = status else: - if params.get("inbox_status", None) and len(params.get("inbox_status")): + if params.get("inbox_status", None) and len(params.get("inbox_status")) and params.get("inbox_status") != 'null': filter["issue_inbox__status__in"] = params.get("inbox_status") return filter @@ -288,11 +308,12 @@ def filter_sub_issue_toggle(params, filter, method): def filter_subscribed_issues(params, filter, method): if method == "GET": - subscribers = params.get("subscriber").split(",") + subscribers = [item for item in params.get("subscriber").split(",") if item != 'null'] + subscribers = filter_valid_uuids(subscribers) if len(subscribers) and "" not in subscribers: filter["issue_subscribers__subscriber_id__in"] = subscribers else: - if params.get("subscriber", None) and len(params.get("subscriber")): + if params.get("subscriber", None) and len(params.get("subscriber")) and params.get("subscriber") != 'null': filter["issue_subscribers__subscriber_id__in"] = params.get("subscriber") return filter @@ -306,7 +327,7 @@ def filter_start_target_date_issues(params, filter, method): def issue_filters(query_params, method): - filter = dict() + filter = {} ISSUE_FILTER = { "state": filter_state, @@ -316,6 +337,7 @@ def issue_filters(query_params, method): "parent": filter_parent, "labels": filter_labels, "assignees": filter_assignees, + "mentions": filter_mentions, "created_by": filter_created_by, "name": filter_name, "created_at": filter_created_at, @@ -329,7 +351,7 @@ def issue_filters(query_params, method): "module": filter_module, "inbox_status": filter_inbox_status, "sub_issue": filter_sub_issue_toggle, - "subscriber": filter_subscribed_issues, + "subscriber": filter_subscribed_issues, "start_target_date": filter_start_target_date_issues, } diff --git a/apiserver/plane/utils/markdown.py b/apiserver/plane/utils/markdown.py index 15d5b4dce62..188c54fec3b 100644 --- a/apiserver/plane/utils/markdown.py +++ b/apiserver/plane/utils/markdown.py @@ -1,3 +1,3 @@ import mistune -markdown = mistune.Markdown() \ No newline at end of file +markdown = mistune.Markdown() diff --git a/apiserver/plane/utils/paginator.py b/apiserver/plane/utils/paginator.py index b3c50abd199..544ed8fef95 100644 --- a/apiserver/plane/utils/paginator.py +++ b/apiserver/plane/utils/paginator.py @@ -21,12 +21,7 @@ def __eq__(self, other): ) def __repr__(self): - return "<{}: value={} offset={} is_prev={}>".format( - type(self).__name__, - self.value, - self.offset, - int(self.is_prev), - ) + return f"{type(self).__name__,}: value={self.value} offset={self.offset}, is_prev={int(self.is_prev)}" def __bool__(self): return bool(self.has_results) @@ -176,10 +171,6 @@ def paginate( **paginator_kwargs, ): """Paginate the request""" - assert (paginator and not paginator_kwargs) or ( - paginator_cls and paginator_kwargs - ) - per_page = self.get_per_page(request, default_per_page, max_per_page) # Convert the cursor value to integer and float from string diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index ca9d881ef49..249b29d48c2 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,36 +1,37 @@ # base requirements -Django==4.2.3 +Django==4.2.5 django-braces==1.15.0 django-taggit==4.0.0 -psycopg==3.1.9 +psycopg==3.1.10 django-oauth-toolkit==2.3.0 mistune==3.0.1 djangorestframework==3.14.0 redis==4.6.0 django-nested-admin==4.0.2 -django-cors-headers==4.1.0 +django-cors-headers==4.2.0 whitenoise==6.5.0 -django-allauth==0.54.0 +django-allauth==0.55.2 faker==18.11.2 django-filter==23.2 jsonmodels==2.6.0 -djangorestframework-simplejwt==5.2.2 -sentry-sdk==1.27.0 +djangorestframework-simplejwt==5.3.0 +sentry-sdk==1.30.0 django-s3-storage==0.14.0 django-crum==0.7.9 django-guardian==2.4.0 dj_rest_auth==2.2.5 -google-auth==2.21.0 -google-api-python-client==2.92.0 +google-auth==2.22.0 +google-api-python-client==2.97.0 django-redis==5.3.0 -uvicorn==0.22.0 +uvicorn==0.23.2 channels==4.0.0 -openai==0.27.8 +openai==0.28.0 slack-sdk==3.21.3 -celery==5.3.1 +celery==5.3.4 django_celery_beat==2.5.0 -psycopg-binary==3.1.9 -psycopg-c==3.1.9 +psycopg-binary==3.1.10 +psycopg-c==3.1.10 scout-apm==2.26.1 -openpyxl==3.1.2 \ No newline at end of file +openpyxl==3.1.2 +beautifulsoup4==4.12.2 \ No newline at end of file diff --git a/apiserver/requirements/production.txt b/apiserver/requirements/production.txt index 4da619d491b..5e3483a96ec 100644 --- a/apiserver/requirements/production.txt +++ b/apiserver/requirements/production.txt @@ -1,11 +1,11 @@ -r base.txt -dj-database-url==2.0.0 -gunicorn==20.1.0 +dj-database-url==2.1.0 +gunicorn==21.2.0 whitenoise==6.5.0 -django-storages==1.13.2 -boto3==1.27.0 -django-anymail==10.0 +django-storages==1.14 +boto3==1.28.40 +django-anymail==10.1 django-debug-toolbar==4.1.0 gevent==23.7.0 psycogreen==1.0.2 \ No newline at end of file diff --git a/apiserver/runtime.txt b/apiserver/runtime.txt index d5831c54fb3..dfe813b8606 100644 --- a/apiserver/runtime.txt +++ b/apiserver/runtime.txt @@ -1 +1 @@ -python-3.11.5 \ No newline at end of file +python-3.11.6 \ No newline at end of file diff --git a/apps/app/.eslintrc.js b/apps/app/.eslintrc.js deleted file mode 100644 index 38e6a5f4c82..00000000000 --- a/apps/app/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - root: true, - extends: ["custom"], - rules: { - "@next/next/no-img-element": "off", - }, -}; diff --git a/apps/app/.prettierrc b/apps/app/.prettierrc deleted file mode 100644 index d5cb26e5447..00000000000 --- a/apps/app/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "printWidth": 100, - "tabWidth": 2, - "trailingComma": "es5" -} diff --git a/apps/app/Dockerfile.dev b/apps/app/Dockerfile.dev deleted file mode 100644 index 789d01af0f0..00000000000 --- a/apps/app/Dockerfile.dev +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:18-alpine -RUN sed -i 's/dl-cdn.alpinelinux.org/mirror.tuna.tsinghua.edu.cn/g' /etc/apk/repositories -RUN apk add --no-cache libc6-compat -# Set working directory -WORKDIR /app - - -COPY . . -RUN yarn config set registry https://registry.npm.taobao.org/ -RUN yarn config set sentrycli_cdnurl https://npmmirror.com/mirrors/sentry-cli/ -#RUN yarn --update-checksums -RUN yarn global add turbo -# RUN yarn add sentry-cli --registry=https://cdn.npm.taobao.org/dist/sentry-cli -#RUN npm set ENTRYCLI_CDNURL=https://cdn.npm.taobao.org/dist/sentry-cli -#RUN npm set sentrycli_cdnurl=https://cdn.npm.taobao.org/dist/sentry-cli - -#RUN yarn install --network-timeout 500000 --silent 2>&1 >/dev/null | grep "error" - -RUN yarn install --network-timeout 500000 --severity error - -EXPOSE 3000 -CMD ["yarn","dev"] diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx deleted file mode 100644 index 3a94263f781..00000000000 --- a/apps/app/components/account/email-code-form.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -// ui -import { CheckCircleIcon } from "@heroicons/react/20/solid"; -import { Input, PrimaryButton, SecondaryButton } from "components/ui"; -// services -import authenticationService from "services/authentication.service"; -import useToast from "hooks/use-toast"; -import useTimer from "hooks/use-timer"; -// icons - -// types -type EmailCodeFormValues = { - email: string; - key?: string; - token?: string; -}; - -export const EmailCodeForm = ({ handleSignIn }: any) => { - const [codeSent, setCodeSent] = useState(false); - const [codeResent, setCodeResent] = useState(false); - const [isCodeResending, setIsCodeResending] = useState(false); - const [errorResendingCode, setErrorResendingCode] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - const { setToastAlert } = useToast(); - const { timer: resendCodeTimer, setTimer: setResendCodeTimer } = useTimer(); - - const { - register, - handleSubmit, - setError, - setValue, - getValues, - watch, - formState: { errors, isSubmitting, isValid, isDirty }, - } = useForm({ - defaultValues: { - email: "", - key: "", - token: "", - }, - mode: "onChange", - reValidateMode: "onChange", - }); - - const isResendDisabled = - resendCodeTimer > 0 || isCodeResending || isSubmitting || errorResendingCode; - - const onSubmit = async ({ email }: EmailCodeFormValues) => { - setErrorResendingCode(false); - await authenticationService - .emailCode({ email }) - .then((res) => { - setValue("key", res.key); - setCodeSent(true); - }) - .catch((err) => { - setErrorResendingCode(true); - setToastAlert({ - title: "Oops!", - type: "error", - message: err?.error, - }); - }); - }; - - const handleSignin = async (formData: EmailCodeFormValues) => { - setIsLoading(true); - await authenticationService - .magicSignIn(formData) - .then((response) => { - handleSignIn(response); - }) - .catch((error) => { - setIsLoading(false); - setToastAlert({ - title: "Oops!", - type: "error", - message: error?.response?.data?.error ?? "Enter the correct code to sign in", - }); - setError("token" as keyof EmailCodeFormValues, { - type: "manual", - message: error?.error, - }); - }); - }; - - const emailOld = getValues("email"); - - useEffect(() => { - setErrorResendingCode(false); - }, [emailOld]); - - useEffect(() => { - const submitForm = (e: KeyboardEvent) => { - if (!codeSent && e.key === "Enter") { - e.preventDefault(); - handleSubmit(onSubmit)().then(() => { - setResendCodeTimer(30); - }); - } - }; - - if (!codeSent) { - window.addEventListener("keydown", submitForm); - } - - return () => { - window.removeEventListener("keydown", submitForm); - }; - }, [handleSubmit, codeSent]); - - return ( - <> - {(codeSent || codeResent) && ( -

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

- )} -
-
- - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - value - ) || "电子邮件地址无效", - }} - error={errors.email} - placeholder="请输入您的电子邮件地址..." - className="border-custom-border-300 h-[46px]" - /> -
- - {codeSent && ( - <> - - - - )} - {codeSent ? ( - - {isLoading ? "登录中..." : "登录"} - - ) : ( - { - handleSubmit(onSubmit)().then(() => { - setResendCodeTimer(30); - }); - }} - disabled={!isValid && isDirty} - loading={isSubmitting} - > - {isSubmitting ? "发送新代码..." : "发送验证码"} - - )} -
- - ); -}; diff --git a/apps/app/components/account/email-password-form.tsx b/apps/app/components/account/email-password-form.tsx deleted file mode 100644 index 950ca5aeb3a..00000000000 --- a/apps/app/components/account/email-password-form.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React, { useState } from "react"; - -import { useRouter } from "next/router"; -import Link from "next/link"; - -// react hook form -import { useForm } from "react-hook-form"; -// components -import { EmailResetPasswordForm } from "components/account"; -// ui -import { Input, PrimaryButton } from "components/ui"; -// types -type EmailPasswordFormValues = { - email: string; - password?: string; - medium?: string; -}; - -type Props = { - onSubmit: (formData: EmailPasswordFormValues) => Promise; -}; - -export const EmailPasswordForm: React.FC = ({ onSubmit }) => { - const [isResettingPassword, setIsResettingPassword] = useState(false); - - const router = useRouter(); - const isSignUpPage = router.pathname === "/sign-up"; - - const { - register, - handleSubmit, - formState: { errors, isSubmitting, isValid, isDirty }, - } = useForm({ - defaultValues: { - email: "", - password: "", - medium: "email", - }, - mode: "onChange", - reValidateMode: "onChange", - }); - - return ( - <> -

- {isResettingPassword - ? "重置密码" - : isSignUpPage - ? "注册MissionPlan" - : "登录MissionPlan"} -

- {isResettingPassword ? ( - - ) : ( -
-
- - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - value - ) || "电子邮件地址无效", - }} - error={errors.email} - placeholder="输入您的电子邮件地址..." - className="border-custom-border-300 h-[46px]" - /> -
-
- -
-
- {isSignUpPage ? ( - - - 已有账号?去登录 - - - ) : ( - - )} -
-
- - {isSignUpPage - ? isSubmitting - ? "注册中..." - : "注册" - : isSubmitting - ? "登录中..." - : "登录"} - - {!isSignUpPage && ( - - - 还没有账号?去注册 - - - )} -
-

演示账号:captain@plane.so

-

密码:password123

-
- )} - - ); -}; diff --git a/apps/app/components/account/email-reset-password-form.tsx b/apps/app/components/account/email-reset-password-form.tsx deleted file mode 100644 index 436760ff846..00000000000 --- a/apps/app/components/account/email-reset-password-form.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from "react"; - -// react hook form -import { useForm } from "react-hook-form"; -// services -import userService from "services/user.service"; -// hooks -import useToast from "hooks/use-toast"; -// ui -import { Input, PrimaryButton, SecondaryButton } from "components/ui"; -// types -type Props = { - setIsResettingPassword: React.Dispatch>; -}; - -export const EmailResetPasswordForm: React.FC = ({ setIsResettingPassword }) => { - const { setToastAlert } = useToast(); - - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ - defaultValues: { - email: "", - }, - mode: "onChange", - reValidateMode: "onChange", - }); - - const forgotPassword = async (formData: any) => { - const payload = { - email: formData.email, - }; - - await userService - .forgotPassword(payload) - .then(() => - setToastAlert({ - type: "success", - title: "成功!", - message: "密码重置链接已发送至您的电子邮件地址。", - }) - ) - .catch((err) => { - if (err.status === 400) - setToastAlert({ - type: "error", - title: "错误!", - message: "请核对输入的电子邮件 ID。", - }); - else - setToastAlert({ - type: "error", - title: "错误!", - message: "出错了。请重试。", - }); - }); - }; - - return ( -
-
- - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - value - ) || "Email address is not valid", - }} - error={errors.email} - placeholder="请输入注册的电子邮件地址.." - className="border-custom-border-300 h-[46px]" - /> -
-
- setIsResettingPassword(false)} - > - Go Back - - - {isSubmitting ? "发送链接..." : "发送重置链接"} - -
-
- ); -}; diff --git a/apps/app/components/account/github-login-button.tsx b/apps/app/components/account/github-login-button.tsx deleted file mode 100644 index 30eb3d0df5a..00000000000 --- a/apps/app/components/account/github-login-button.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useEffect, useState, FC } from "react"; - -import Link from "next/link"; -import Image from "next/image"; -import { useRouter } from "next/router"; - -// next-themes -import { useTheme } from "next-themes"; -// images -import githubBlackImage from "/public/logos/github-black.png"; -import githubWhiteImage from "/public/logos/github-white.png"; - -const { NEXT_PUBLIC_GITHUB_ID } = process.env; - -export interface GithubLoginButtonProps { - handleSignIn: React.Dispatch; -} - -export const GithubLoginButton: FC = ({ handleSignIn }) => { - const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); - const [gitCode, setGitCode] = useState(null); - - const { - query: { code }, - } = useRouter(); - - const { theme } = useTheme(); - - useEffect(() => { - if (code && !gitCode) { - setGitCode(code.toString()); - handleSignIn(code.toString()); - } - }, [code, gitCode, handleSignIn]); - - useEffect(() => { - const origin = - typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - setLoginCallBackURL(`${origin}/` as any); - }, []); - - return ( -
- - - -
- ); -}; diff --git a/apps/app/components/account/google-login.tsx b/apps/app/components/account/google-login.tsx deleted file mode 100644 index c814c658c06..00000000000 --- a/apps/app/components/account/google-login.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react"; - -import Script from "next/script"; - -export interface IGoogleLoginButton { - text?: string; - handleSignIn: React.Dispatch; - styles?: CSSProperties; -} - -export const GoogleLoginButton: FC = ({ handleSignIn }) => { - const googleSignInButton = useRef(null); - const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false); - - const loadScript = useCallback(() => { - if (!googleSignInButton.current || gsiScriptLoaded) return; - - window?.google?.accounts.id.initialize({ - client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "", - callback: handleSignIn, - }); - - try { - window?.google?.accounts.id.renderButton( - googleSignInButton.current, - { - type: "standard", - theme: "outline", - size: "large", - logo_alignment: "center", - width: 360, - text: "signin_with", - } as GsiButtonConfiguration // customization attributes - ); - } catch (err) { - console.log(err); - } - - window?.google?.accounts.id.prompt(); // also display the One Tap dialog - - setGsiScriptLoaded(true); - }, [handleSignIn, gsiScriptLoaded]); - - useEffect(() => { - if (window?.google?.accounts?.id) { - loadScript(); - } - return () => { - window?.google?.accounts.id.cancel(); - }; - }, [loadScript]); - - return ( - <> - + )} + + +
+ + {process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && ( + + )} + + + ); + } +} + +export default MyDocument; diff --git a/apps/app/pages/_error.tsx b/web/pages/_error.tsx similarity index 78% rename from apps/app/pages/_error.tsx rename to web/pages/_error.tsx index 12ce38b69d9..094d90601bd 100644 --- a/apps/app/pages/_error.tsx +++ b/web/pages/_error.tsx @@ -3,13 +3,16 @@ import * as Sentry from "@sentry/nextjs"; import { useRouter } from "next/router"; // services -import authenticationService from "services/authentication.service"; +import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; // layouts import DefaultLayout from "layouts/default-layout"; // ui -import { PrimaryButton, SecondaryButton } from "components/ui"; +import { Button } from "@plane/ui"; + +// services +const authService = new AuthService(); const CustomErrorComponent = () => { const router = useRouter(); @@ -17,7 +20,7 @@ const CustomErrorComponent = () => { const { setToastAlert } = useToast(); const handleSignOut = async () => { - await authenticationService + await authService .signOut() .catch(() => setToastAlert({ @@ -36,9 +39,8 @@ const CustomErrorComponent = () => {

Exception Detected!

- We{"'"}re Sorry! An exception has been detected, and our engineering team has been - notified. We apologize for any inconvenience this may have caused. Please reach out to - our engineering team at{" "} + We{"'"}re Sorry! An exception has been detected, and our engineering team has been notified. We apologize + for any inconvenience this may have caused. Please reach out to our engineering team at{" "} support@plane.so {" "} @@ -55,12 +57,12 @@ const CustomErrorComponent = () => {

- router.back()}> + +
diff --git a/web/pages/accounts/forgot-password.tsx b/web/pages/accounts/forgot-password.tsx new file mode 100644 index 00000000000..c1869439347 --- /dev/null +++ b/web/pages/accounts/forgot-password.tsx @@ -0,0 +1,75 @@ +import { ReactElement } from "react"; +import Image from "next/image"; +// components +import { EmailForgotPasswordForm, EmailForgotPasswordFormValues } from "components/account"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// services +import { UserService } from "services/user.service"; +// hooks +import useToast from "hooks/use-toast"; +// images +import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; +// types +import { NextPageWithLayout } from "types/app"; + +const userService = new UserService(); + +const ForgotPasswordPage: NextPageWithLayout = () => { + // toast + const { setToastAlert } = useToast(); + + const handleForgotPassword = (formData: EmailForgotPasswordFormValues) => { + const payload = { + email: formData.email, + }; + + return userService + .forgotPassword(payload) + .then(() => + setToastAlert({ + type: "success", + title: "Success!", + message: "Password reset link has been sent to your email address.", + }) + ) + .catch((err) => { + if (err.status === 400) + setToastAlert({ + type: "error", + title: "Error!", + message: "Please check the Email ID entered.", + }); + else + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again.", + }); + }); + }; + return ( + <> +
+
+
+
+ Plane Logo +
+
+
+
+
+

Forgot Password

+ +
+
+ + ); +}; + +ForgotPasswordPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default ForgotPasswordPage; diff --git a/web/pages/accounts/magic-sign-in.tsx b/web/pages/accounts/magic-sign-in.tsx new file mode 100644 index 00000000000..fc698fef1af --- /dev/null +++ b/web/pages/accounts/magic-sign-in.tsx @@ -0,0 +1,105 @@ +import { useState, useEffect, ReactElement } from "react"; +import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// services +import { AuthService } from "services/auth.service"; +// hooks +import useUserAuth from "hooks/use-user-auth"; +import useToast from "hooks/use-toast"; +// types +import { NextPageWithLayout } from "types/app"; + +const authService = new AuthService(); + +const MagicSignInPage: NextPageWithLayout = () => { + const router = useRouter(); + const { password, key } = router.query; + + const { setToastAlert } = useToast(); + + const { setTheme } = useTheme(); + + const { mutateUser } = useUserAuth("sign-in"); + + const [isSigningIn, setIsSigningIn] = useState(false); + const [errorSigningIn, setErrorSignIn] = useState(); + + useEffect(() => { + setTheme("system"); + }, [setTheme]); + + useEffect(() => { + setIsSigningIn(() => false); + setErrorSignIn(() => undefined); + if (!password || !key) { + setErrorSignIn("URL is invalid"); + return; + } else { + setIsSigningIn(() => true); + authService + .magicSignIn({ token: password, key }) + .then(async () => { + setIsSigningIn(false); + await mutateUser(); + }) + .catch((err) => { + setErrorSignIn(err.response.data.error); + setIsSigningIn(false); + }); + } + }, [password, key, mutateUser, router]); + + return ( +
+ {isSigningIn ? ( +
+

Signing you in...

+

Please wait while we are preparing your take off.

+
+ ) : errorSigningIn ? ( +
+

Error

+
+
{errorSigningIn}.
+ { + authService + .emailCode({ email: (key as string).split("_")[1] }) + .then(() => { + setToastAlert({ + type: "success", + title: "Email sent", + message: "A new link/code has been send to you.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error", + message: "Unable to send email.", + }); + }); + }} + > + Send link again? + +
+
+ ) : ( +
+

Success

+

Redirecting you to the app...

+
+ )} +
+ ); +}; + +MagicSignInPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default MagicSignInPage; diff --git a/web/pages/accounts/reset-password.tsx b/web/pages/accounts/reset-password.tsx new file mode 100644 index 00000000000..a817509d1c5 --- /dev/null +++ b/web/pages/accounts/reset-password.tsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState, ReactElement } from "react"; +import { useRouter } from "next/router"; +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { Controller, useForm } from "react-hook-form"; +// hooks +import useToast from "hooks/use-toast"; +// services +import { UserService } from "services/user.service"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// ui +import { Button, Input, Spinner } from "@plane/ui"; +// images +import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; +// types +import { NextPageWithLayout } from "types/app"; + +type FormData = { + password: string; + confirmPassword: string; +}; + +// services +const userService = new UserService(); + +const ResetPasswordPage: NextPageWithLayout = () => { + const [isLoading, setIsLoading] = useState(true); + + const router = useRouter(); + const { uidb64, token } = router.query; + + const { setToastAlert } = useToast(); + + const { setTheme } = useTheme(); + + const { + handleSubmit, + control, + formState: { errors, isSubmitting }, + } = useForm(); + + const onSubmit = async (formData: FormData) => { + if (!uidb64 || !token) return; + + if (formData.password !== formData.confirmPassword) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Passwords do not match.", + }); + + return; + } + + const payload = { + new_password: formData.password, + confirm_password: formData.confirmPassword, + }; + + await userService + .resetPassword(uidb64.toString(), token.toString(), payload) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Password reset successfully. You can now login with your new password.", + }); + router.push("/"); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error || "Something went wrong. Please try again later or contact the support team.", + }) + ); + }; + + useEffect(() => { + setTheme("system"); + }, [setTheme]); + + useEffect(() => { + if (parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0")) router.push("/"); + else setIsLoading(false); + }, [router]); + + if (isLoading) + return ( +
+ +
+ ); + + return ( + <> +
+
+
+
+ Plane Logo +
+
+
+
+
+

Reset your password

+
+
+ ( + + )} + /> +
+
+ ( + + )} + /> +
+ +
+
+
+ + ); +}; + +ResetPasswordPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default ResetPasswordPage; diff --git a/web/pages/accounts/sign-up.tsx b/web/pages/accounts/sign-up.tsx new file mode 100644 index 00000000000..dac86c22bf5 --- /dev/null +++ b/web/pages/accounts/sign-up.tsx @@ -0,0 +1,93 @@ +import React, { useEffect, ReactElement } from "react"; +import Image from "next/image"; +import { useRouter } from "next/router"; +// next-themes +import { useTheme } from "next-themes"; +// services +import { AuthService } from "services/auth.service"; +// hooks +import useUserAuth from "hooks/use-user-auth"; +import useToast from "hooks/use-toast"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// components +import { EmailSignUpForm } from "components/account"; +// images +import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; +// types +import { NextPageWithLayout } from "types/app"; + +type EmailPasswordFormValues = { + email: string; + password?: string; + medium?: string; +}; + +// services +const authService = new AuthService(); + +const SignUpPage: NextPageWithLayout = () => { + const router = useRouter(); + + const { setToastAlert } = useToast(); + + const { setTheme } = useTheme(); + + const { mutateUser } = useUserAuth("sign-in"); + + const handleSignUp = async (formData: EmailPasswordFormValues) => { + const payload = { + email: formData.email, + password: formData.password ?? "", + }; + + await authService + .emailSignUp(payload) + .then(async (response) => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Account created successfully.", + }); + + if (response) await mutateUser(); + router.push("/onboarding"); + }) + .catch((err) => + setToastAlert({ + type: "error", + title: "Error!", + message: err?.error || "Something went wrong. Please try again later or contact the support team.", + }) + ); + }; + + useEffect(() => { + setTheme("system"); + }, [setTheme]); + + return ( + <> +
+
+
+
+ Plane Logo +
+
+
+
+
+

SignUp on Plane

+ +
+
+ + ); +}; + +SignUpPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default SignUpPage; diff --git a/web/pages/api/track-event.ts b/web/pages/api/track-event.ts new file mode 100644 index 00000000000..09ef11d3604 --- /dev/null +++ b/web/pages/api/track-event.ts @@ -0,0 +1,34 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +// jitsu +import { createClient } from "@jitsu/nextjs"; + +const jitsuClient = createClient({ + key: process.env.JITSU_TRACKER_ACCESS_KEY || "", + tracking_host: process.env.JITSU_TRACKER_HOST || "", +}); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { eventName, user, extra } = req.body; + + if (!eventName) { + return res.status(400).json({ message: "错误请求" }); + } + + if (!user) return res.status(401).json({ message: "未经授权" }); + + jitsuClient + .id({ + id: user?.id, + email: user?.email, + first_name: user?.first_name, + last_name: user?.last_name, + display_name: user?.display_name, + }) + .then(() => { + jitsuClient.track(eventName, { + ...extra, + }); + }); + + res.status(200).json({ message: "成功" }); +} diff --git a/web/pages/api/unsplash.ts b/web/pages/api/unsplash.ts new file mode 100644 index 00000000000..11acb2096fd --- /dev/null +++ b/web/pages/api/unsplash.ts @@ -0,0 +1,26 @@ +import axios from "axios"; +import type { NextApiRequest, NextApiResponse } from "next"; + +const unsplashKey = process.env.UNSPLASH_ACCESS_KEY; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { query, page, per_page = 20 } = req.query; + + const url = query + ? `https://api.unsplash.com/search/photos/?client_id=${unsplashKey}&query=${query}&page=${page}&per_page=${per_page}` + : `https://api.unsplash.com/photos/?client_id=${unsplashKey}&page=${page}&per_page=${per_page}`; + + const response = await axios({ + method: "GET", + url, + headers: { + "Content-Type": "application/json", + }, + }); + + res.status(200).json(response?.data); + } catch (error) { + res.status(500).json({ message: "Failed to fetch unsplash", error }); + } +} diff --git a/web/pages/create-workspace.tsx b/web/pages/create-workspace.tsx new file mode 100644 index 00000000000..51ae06b9e22 --- /dev/null +++ b/web/pages/create-workspace.tsx @@ -0,0 +1,84 @@ +import React, { useState, ReactElement } from "react"; +import { useRouter } from "next/router"; +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// layouts +import DefaultLayout from "layouts/default-layout"; +import { UserAuthWrapper } from "layouts/auth-layout"; +// components +import { CreateWorkspaceForm } from "components/workspace"; +// images +import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; +import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; +// types +import { IWorkspace } from "types"; +import { NextPageWithLayout } from "types/app"; + +const CreateWorkspacePage: NextPageWithLayout = observer(() => { + const [defaultValues, setDefaultValues] = useState({ + name: "", + slug: "", + organization_size: "", + }); + + const router = useRouter(); + + const { user: userStore } = useMobxStore(); + const user = userStore.currentUser; + + const { theme } = useTheme(); + + const onSubmit = async (workspace: IWorkspace) => { + await userStore + .updateCurrentUser({ last_workspace_id: workspace.id }) + .then(() => router.push(`/${workspace.slug}`)); + }; + + return ( +
+
+
+ +
+ {user?.email} +
+
+
+
+

Create your workspace

+
+ +
+
+
+
+ ); +}); + +CreateWorkspacePage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default CreateWorkspacePage; diff --git a/web/pages/index.tsx b/web/pages/index.tsx new file mode 100644 index 00000000000..c6842a921f0 --- /dev/null +++ b/web/pages/index.tsx @@ -0,0 +1,15 @@ +import { ReactElement } from "react"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// components +import { SignInView } from "components/page-views"; +// type +import { NextPageWithLayout } from "types/app"; + +const HomePage: NextPageWithLayout = () => ; + +HomePage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default HomePage; diff --git a/web/pages/installations/[provider]/index.tsx b/web/pages/installations/[provider]/index.tsx new file mode 100644 index 00000000000..243065a7c60 --- /dev/null +++ b/web/pages/installations/[provider]/index.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, ReactElement } from "react"; +import { useRouter } from "next/router"; +// services +import { AppInstallationService } from "services/app_installation.service"; +// ui +import { Spinner } from "@plane/ui"; +// types +import { NextPageWithLayout } from "types/app"; + +// services +const appInstallationService = new AppInstallationService(); + +const AppPostInstallation: NextPageWithLayout = () => { + const router = useRouter(); + const { installation_id, state, provider, code } = router.query; + + useEffect(() => { + if (provider === "github" && state && installation_id) { + appInstallationService + .addInstallationApp(state.toString(), provider, { installation_id }) + .then(() => { + window.opener = null; + window.open("", "_self"); + window.close(); + }) + .catch((err) => { + console.log(err); + }); + } else if (provider === "slack" && state && code) { + const [workspaceSlug, projectId, integrationId] = state.toString().split(","); + + 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]); + + return ( +
+

Installing. Please wait...

+ +
+ ); +}; + +AppPostInstallation.getLayout = function getLayout(page: ReactElement) { + return
{page}
; +}; + +export default AppPostInstallation; diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx new file mode 100644 index 00000000000..d950b13db4d --- /dev/null +++ b/web/pages/invitations/index.tsx @@ -0,0 +1,219 @@ +import React, { useState, ReactElement } from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import useSWR, { mutate } from "swr"; +import { useTheme } from "next-themes"; +// services +import { WorkspaceService } from "services/workspace.service"; +import { UserService } from "services/user.service"; +// hooks +import useUser from "hooks/use-user"; +import useToast from "hooks/use-toast"; +// layouts +import DefaultLayout from "layouts/default-layout"; +import { UserAuthWrapper } from "layouts/auth-layout"; +// ui +import { Button } from "@plane/ui"; +// icons +import { CheckCircle2 } from "lucide-react"; +// images +import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; +import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; +import emptyInvitation from "public/empty-state/invitation.svg"; +// helpers +import { truncateText } from "helpers/string.helper"; +// types +import { NextPageWithLayout } from "types/app"; +import type { IWorkspaceMemberInvitation } from "types"; +// constants +import { ROLE } from "constants/workspace"; +// components +import { EmptyState } from "components/common"; + +// services +const workspaceService = new WorkspaceService(); +const userService = new UserService(); + +const UserInvitationsPage: NextPageWithLayout = () => { + const [invitationsRespond, setInvitationsRespond] = useState([]); + const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); + + const router = useRouter(); + + const { theme } = useTheme(); + + const { user } = useUser(); + + const { setToastAlert } = useToast(); + + const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS", () => + workspaceService.userWorkspaceInvitations() + ); + + const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => { + if (action === "accepted") { + setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]); + } else if (action === "withdraw") { + setInvitationsRespond((prevData) => prevData.filter((item: string) => item !== workspace_invitation.id)); + } + }; + + const submitInvitations = () => { + if (invitationsRespond.length === 0) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Please select at least one invitation.", + }); + return; + } + + setIsJoiningWorkspaces(true); + + workspaceService + .joinWorkspaces({ invitations: invitationsRespond }) + .then(() => { + mutate("USER_WORKSPACES"); + const firstInviteId = invitationsRespond[0]; + const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace; + userService + .updateUser({ last_workspace_id: redirectWorkspace?.id }) + .then(() => { + setIsJoiningWorkspaces(false); + router.push(`/${redirectWorkspace?.slug}`); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong, Please try again.", + }); + setIsJoiningWorkspaces(false); + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong, Please try again.", + }); + setIsJoiningWorkspaces(false); + }); + }; + + return ( +
+
+
+
+
+ {theme === "light" ? ( + Plane black logo + ) : ( + Plane white logo + )} +
+
+
+ {user?.email} +
+
+ {invitations ? ( + invitations.length > 0 ? ( +
+
+
We see that someone has invited you to
+

Join a workspace

+
+ {invitations.map((invitation) => { + const isSelected = invitationsRespond.includes(invitation.id); + + return ( +
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")} + > +
+
+ {invitation.workspace.logo && invitation.workspace.logo !== "" ? ( + {invitation.workspace.name} + ) : ( + + {invitation.workspace.name[0]} + + )} +
+
+
+
{truncateText(invitation.workspace.name, 30)}
+

{ROLE[invitation.role]}

+
+ + + +
+ ); + })} +
+
+ + + + + + +
+
+
+ ) : ( +
+ router.push("/"), + }} + /> +
+ ) + ) : null} +
+ ); +}; + +UserInvitationsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default UserInvitationsPage; diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx new file mode 100644 index 00000000000..f8c5e3e6fe1 --- /dev/null +++ b/web/pages/onboarding/index.tsx @@ -0,0 +1,195 @@ +import { useEffect, useState, ReactElement } from "react"; +import Image from "next/image"; +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; +import { useTheme } from "next-themes"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// services +import { WorkspaceService } from "services/workspace.service"; +// hooks +import useUserAuth from "hooks/use-user-auth"; +// layouts +import DefaultLayout from "layouts/default-layout"; +import { UserAuthWrapper } from "layouts/auth-layout"; +// components +import { InviteMembers, JoinWorkspaces, UserDetails, Workspace } from "components/onboarding"; +// ui +import { Spinner } from "@plane/ui"; +// images +import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; +import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; +import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; +// types +import { IUser, TOnboardingSteps } from "types"; +import { NextPageWithLayout } from "types/app"; + +// services +const workspaceService = new WorkspaceService(); + +const OnboardingPage: NextPageWithLayout = observer(() => { + const [step, setStep] = useState(null); + + const { user: userStore, workspace: workspaceStore } = useMobxStore(); + + const user = userStore.currentUser ?? undefined; + const workspaces = workspaceStore.workspaces; + const userWorkspaces = workspaceStore.workspacesCreateByCurrentUser; + + const { theme, setTheme } = useTheme(); + + const {} = useUserAuth("onboarding"); + + const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS_LIST", () => + workspaceService.userWorkspaceInvitations() + ); + + // update last active workspace details + const updateLastWorkspace = async () => { + if (!workspaces) return; + + await userStore.updateCurrentUser({ + last_workspace_id: workspaces[0]?.id, + }); + }; + + // handle step change + const stepChange = async (steps: Partial) => { + if (!user) return; + + const payload: Partial = { + onboarding_step: { + ...user.onboarding_step, + ...steps, + }, + }; + + await userStore.updateCurrentUser(payload); + }; + + // complete onboarding + const finishOnboarding = async () => { + if (!user) return; + + await userStore.updateUserOnBoard(); + }; + + useEffect(() => { + setTheme("system"); + }, [setTheme]); + + useEffect(() => { + const handleStepChange = async () => { + if (!user || !invitations) return; + + const onboardingStep = user.onboarding_step; + + if (!onboardingStep.profile_complete && step !== 1) setStep(1); + + if (onboardingStep.profile_complete) { + if (!onboardingStep.workspace_join && invitations.length > 0 && step !== 2 && step !== 4) setStep(4); + else if (!onboardingStep.workspace_create && (step !== 4 || onboardingStep.workspace_join) && step !== 2) + setStep(2); + } + + if ( + onboardingStep.profile_complete && + onboardingStep.workspace_create && + !onboardingStep.workspace_invite && + step !== 3 + ) + setStep(3); + }; + + handleStepChange(); + }, [user, invitations, step]); + + return ( + <> + {user && step !== null ? ( +
+
+
+ {step === 1 ? ( +
+
+ Plane logo +
+
+ ) : ( +
+
+ {theme === "light" ? ( + Plane black logo + ) : ( + Plane white logo + )} +
+
+ )} +
+ {user?.email} +
+
+
+ {step === 1 ? ( + + ) : step === 2 ? ( + + ) : step === 3 ? ( + + ) : ( + step === 4 && ( + + ) + )} +
+ {step !== 4 && ( +
+
+

{step} of 3 steps

+
+
+
+
+
+ )} +
+ ) : ( +
+ +
+ )} + + ); +}); + +OnboardingPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OnboardingPage; diff --git a/web/pages/workspace-invitations/index.tsx b/web/pages/workspace-invitations/index.tsx new file mode 100644 index 00000000000..c2d3bd9e28c --- /dev/null +++ b/web/pages/workspace-invitations/index.tsx @@ -0,0 +1,146 @@ +import React, { ReactElement } from "react"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// swr +import { Boxes, Check, Share2, Star, User2, X } from "lucide-react"; +// services +import { WorkspaceService } from "services/workspace.service"; +// hooks +import useUser from "hooks/use-user"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// ui +import { Spinner } from "@plane/ui"; +// icons +import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space"; +// types +import { NextPageWithLayout } from "types/app"; +// constants +import { WORKSPACE_INVITATION } from "constants/fetch-keys"; + +// services +const workspaceService = new WorkspaceService(); + +const WorkspaceInvitationPage: NextPageWithLayout = () => { + const router = useRouter(); + + const { invitation_id, email } = router.query; + + const { user } = useUser(); + + const { data: invitationDetail, error } = useSWR( + invitation_id && WORKSPACE_INVITATION(invitation_id.toString()), + () => (invitation_id ? workspaceService.getWorkspaceInvitation(invitation_id as string) : null) + ); + + const handleAccept = () => { + if (!invitationDetail) return; + workspaceService + .joinWorkspace( + invitationDetail.workspace.slug, + invitationDetail.id, + { + accepted: true, + email: invitationDetail.email, + }, + user + ) + .then(() => { + if (email === user?.email) { + router.push("/invitations"); + } else { + router.push("/"); + } + }) + .catch((err) => console.error(err)); + }; + + return ( +
+ {invitationDetail ? ( + <> + {error ? ( +
+

INVITATION NOT FOUND

+
+ ) : ( + <> + {invitationDetail.accepted ? ( + <> + + router.push("/")} /> + + + ) : ( + + + { + router.push("/"); + }} + /> + + )} + + )} + + ) : error ? ( + + {!user ? ( + { + router.push("/"); + }} + /> + ) : ( + { + router.push("/"); + }} + /> + )} + { + router.push("https://github.com/makeplane"); + }} + /> + { + router.push("https://discord.com/invite/8SR2N9PAcJ"); + }} + /> + + ) : ( +
+ +
+ )} +
+ ); +}; + +WorkspaceInvitationPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default WorkspaceInvitationPage; diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 00000000000..6887c82624a --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,8 @@ +module.exports = { + plugins: { + "postcss-import": {}, + "tailwindcss/nesting": {}, + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/space/public/404.svg b/web/public/404.svg similarity index 100% rename from apps/space/public/404.svg rename to web/public/404.svg diff --git a/apps/app/public/animated-icons/uploading.json b/web/public/animated-icons/uploading.json similarity index 100% rename from apps/app/public/animated-icons/uploading.json rename to web/public/animated-icons/uploading.json diff --git a/apps/app/public/attachment/audio-icon.png b/web/public/attachment/audio-icon.png similarity index 100% rename from apps/app/public/attachment/audio-icon.png rename to web/public/attachment/audio-icon.png diff --git a/apps/app/public/attachment/css-icon.png b/web/public/attachment/css-icon.png similarity index 100% rename from apps/app/public/attachment/css-icon.png rename to web/public/attachment/css-icon.png diff --git a/apps/app/public/attachment/csv-icon.png b/web/public/attachment/csv-icon.png similarity index 100% rename from apps/app/public/attachment/csv-icon.png rename to web/public/attachment/csv-icon.png diff --git a/apps/app/public/attachment/default-icon.png b/web/public/attachment/default-icon.png similarity index 100% rename from apps/app/public/attachment/default-icon.png rename to web/public/attachment/default-icon.png diff --git a/apps/app/public/attachment/doc-icon.png b/web/public/attachment/doc-icon.png similarity index 100% rename from apps/app/public/attachment/doc-icon.png rename to web/public/attachment/doc-icon.png diff --git a/apps/app/public/attachment/excel-icon.png b/web/public/attachment/excel-icon.png similarity index 100% rename from apps/app/public/attachment/excel-icon.png rename to web/public/attachment/excel-icon.png diff --git a/apps/app/public/attachment/figma-icon.png b/web/public/attachment/figma-icon.png similarity index 100% rename from apps/app/public/attachment/figma-icon.png rename to web/public/attachment/figma-icon.png diff --git a/apps/app/public/attachment/html-icon.png b/web/public/attachment/html-icon.png similarity index 100% rename from apps/app/public/attachment/html-icon.png rename to web/public/attachment/html-icon.png diff --git a/apps/app/public/attachment/img-icon.png b/web/public/attachment/img-icon.png similarity index 100% rename from apps/app/public/attachment/img-icon.png rename to web/public/attachment/img-icon.png diff --git a/apps/app/public/attachment/jpg-icon.png b/web/public/attachment/jpg-icon.png similarity index 100% rename from apps/app/public/attachment/jpg-icon.png rename to web/public/attachment/jpg-icon.png diff --git a/apps/app/public/attachment/js-icon.png b/web/public/attachment/js-icon.png similarity index 100% rename from apps/app/public/attachment/js-icon.png rename to web/public/attachment/js-icon.png diff --git a/apps/app/public/attachment/pdf-icon.png b/web/public/attachment/pdf-icon.png similarity index 100% rename from apps/app/public/attachment/pdf-icon.png rename to web/public/attachment/pdf-icon.png diff --git a/apps/app/public/attachment/png-icon.png b/web/public/attachment/png-icon.png similarity index 100% rename from apps/app/public/attachment/png-icon.png rename to web/public/attachment/png-icon.png diff --git a/apps/app/public/attachment/svg-icon.png b/web/public/attachment/svg-icon.png similarity index 100% rename from apps/app/public/attachment/svg-icon.png rename to web/public/attachment/svg-icon.png diff --git a/apps/app/public/attachment/txt-icon.png b/web/public/attachment/txt-icon.png similarity index 100% rename from apps/app/public/attachment/txt-icon.png rename to web/public/attachment/txt-icon.png diff --git a/apps/app/public/attachment/video-icon.png b/web/public/attachment/video-icon.png similarity index 100% rename from apps/app/public/attachment/video-icon.png rename to web/public/attachment/video-icon.png diff --git a/apps/app/public/auth/project-not-authorized.svg b/web/public/auth/project-not-authorized.svg similarity index 100% rename from apps/app/public/auth/project-not-authorized.svg rename to web/public/auth/project-not-authorized.svg diff --git a/apps/app/public/auth/workspace-not-authorized.svg b/web/public/auth/workspace-not-authorized.svg similarity index 100% rename from apps/app/public/auth/workspace-not-authorized.svg rename to web/public/auth/workspace-not-authorized.svg diff --git a/apps/app/public/empty-state/analytics.svg b/web/public/empty-state/analytics.svg similarity index 100% rename from apps/app/public/empty-state/analytics.svg rename to web/public/empty-state/analytics.svg diff --git a/apps/app/public/empty-state/cycle.svg b/web/public/empty-state/cycle.svg similarity index 100% rename from apps/app/public/empty-state/cycle.svg rename to web/public/empty-state/cycle.svg diff --git a/apps/app/public/empty-state/dashboard.svg b/web/public/empty-state/dashboard.svg similarity index 100% rename from apps/app/public/empty-state/dashboard.svg rename to web/public/empty-state/dashboard.svg diff --git a/apps/app/public/empty-state/empty_bar_graph.svg b/web/public/empty-state/empty_bar_graph.svg similarity index 100% rename from apps/app/public/empty-state/empty_bar_graph.svg rename to web/public/empty-state/empty_bar_graph.svg diff --git a/apps/app/public/empty-state/empty_graph.svg b/web/public/empty-state/empty_graph.svg similarity index 100% rename from apps/app/public/empty-state/empty_graph.svg rename to web/public/empty-state/empty_graph.svg diff --git a/web/public/empty-state/empty_label.svg b/web/public/empty-state/empty_label.svg new file mode 100644 index 00000000000..c664da6f47e --- /dev/null +++ b/web/public/empty-state/empty_label.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/public/empty-state/empty_members.svg b/web/public/empty-state/empty_members.svg new file mode 100644 index 00000000000..6672c587bfd --- /dev/null +++ b/web/public/empty-state/empty_members.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/apps/app/public/empty-state/empty_users.svg b/web/public/empty-state/empty_users.svg similarity index 100% rename from apps/app/public/empty-state/empty_users.svg rename to web/public/empty-state/empty_users.svg diff --git a/apps/app/public/empty-state/estimate.svg b/web/public/empty-state/estimate.svg similarity index 100% rename from apps/app/public/empty-state/estimate.svg rename to web/public/empty-state/estimate.svg diff --git a/apps/app/public/empty-state/integration.svg b/web/public/empty-state/integration.svg similarity index 100% rename from apps/app/public/empty-state/integration.svg rename to web/public/empty-state/integration.svg diff --git a/apps/app/public/empty-state/invitation.svg b/web/public/empty-state/invitation.svg similarity index 100% rename from apps/app/public/empty-state/invitation.svg rename to web/public/empty-state/invitation.svg diff --git a/apps/app/public/empty-state/issue-archive.svg b/web/public/empty-state/issue-archive.svg similarity index 100% rename from apps/app/public/empty-state/issue-archive.svg rename to web/public/empty-state/issue-archive.svg diff --git a/apps/app/public/empty-state/issue.svg b/web/public/empty-state/issue.svg similarity index 100% rename from apps/app/public/empty-state/issue.svg rename to web/public/empty-state/issue.svg diff --git a/apps/app/public/empty-state/label.svg b/web/public/empty-state/label.svg similarity index 100% rename from apps/app/public/empty-state/label.svg rename to web/public/empty-state/label.svg diff --git a/apps/app/public/empty-state/module.svg b/web/public/empty-state/module.svg similarity index 100% rename from apps/app/public/empty-state/module.svg rename to web/public/empty-state/module.svg diff --git a/apps/app/public/empty-state/my-issues.svg b/web/public/empty-state/my-issues.svg similarity index 100% rename from apps/app/public/empty-state/my-issues.svg rename to web/public/empty-state/my-issues.svg diff --git a/apps/app/public/empty-state/notification.svg b/web/public/empty-state/notification.svg similarity index 100% rename from apps/app/public/empty-state/notification.svg rename to web/public/empty-state/notification.svg diff --git a/apps/app/public/empty-state/page.svg b/web/public/empty-state/page.svg similarity index 100% rename from apps/app/public/empty-state/page.svg rename to web/public/empty-state/page.svg diff --git a/apps/app/public/empty-state/project.svg b/web/public/empty-state/project.svg similarity index 100% rename from apps/app/public/empty-state/project.svg rename to web/public/empty-state/project.svg diff --git a/apps/app/public/empty-state/recent_activity.svg b/web/public/empty-state/recent_activity.svg similarity index 100% rename from apps/app/public/empty-state/recent_activity.svg rename to web/public/empty-state/recent_activity.svg diff --git a/apps/app/public/empty-state/state_graph.svg b/web/public/empty-state/state_graph.svg similarity index 93% rename from apps/app/public/empty-state/state_graph.svg rename to web/public/empty-state/state_graph.svg index 07337991ee1..651ea347498 100644 --- a/apps/app/public/empty-state/state_graph.svg +++ b/web/public/empty-state/state_graph.svg @@ -2,25 +2,25 @@ - + - + - + - + - + - + diff --git a/apps/app/public/empty-state/view.svg b/web/public/empty-state/view.svg similarity index 100% rename from apps/app/public/empty-state/view.svg rename to web/public/empty-state/view.svg diff --git a/web/public/favicon/android-chrome-192x192.png b/web/public/favicon/android-chrome-192x192.png new file mode 100644 index 00000000000..62e95acfc55 Binary files /dev/null and b/web/public/favicon/android-chrome-192x192.png differ diff --git a/web/public/favicon/android-chrome-512x512.png b/web/public/favicon/android-chrome-512x512.png new file mode 100644 index 00000000000..41400832b31 Binary files /dev/null and b/web/public/favicon/android-chrome-512x512.png differ diff --git a/web/public/favicon/apple-touch-icon.png b/web/public/favicon/apple-touch-icon.png new file mode 100644 index 00000000000..5273d4951db Binary files /dev/null and b/web/public/favicon/apple-touch-icon.png differ diff --git a/web/public/favicon/favicon-16x16.png b/web/public/favicon/favicon-16x16.png new file mode 100644 index 00000000000..8ddbd49c043 Binary files /dev/null and b/web/public/favicon/favicon-16x16.png differ diff --git a/web/public/favicon/favicon-32x32.png b/web/public/favicon/favicon-32x32.png new file mode 100644 index 00000000000..80cbe7a68fd Binary files /dev/null and b/web/public/favicon/favicon-32x32.png differ diff --git a/web/public/favicon/favicon.ico b/web/public/favicon/favicon.ico new file mode 100644 index 00000000000..9094a07c786 Binary files /dev/null and b/web/public/favicon/favicon.ico differ diff --git a/web/public/favicon/site.webmanifest b/web/public/favicon/site.webmanifest new file mode 100644 index 00000000000..45dc8a20658 --- /dev/null +++ b/web/public/favicon/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/apps/app/public/logos/github-black.png b/web/public/logos/github-black.png similarity index 100% rename from apps/app/public/logos/github-black.png rename to web/public/logos/github-black.png diff --git a/apps/app/public/logos/github-square.png b/web/public/logos/github-square.png similarity index 100% rename from apps/app/public/logos/github-square.png rename to web/public/logos/github-square.png diff --git a/apps/app/public/logos/github-white.png b/web/public/logos/github-white.png similarity index 100% rename from apps/app/public/logos/github-white.png rename to web/public/logos/github-white.png diff --git a/apps/app/public/mac-command.svg b/web/public/mac-command.svg similarity index 100% rename from apps/app/public/mac-command.svg rename to web/public/mac-command.svg diff --git a/apps/app/public/onboarding/cycles.svg b/web/public/onboarding/cycles.svg similarity index 100% rename from apps/app/public/onboarding/cycles.svg rename to web/public/onboarding/cycles.svg diff --git a/apps/app/public/onboarding/cycles.webp b/web/public/onboarding/cycles.webp similarity index 100% rename from apps/app/public/onboarding/cycles.webp rename to web/public/onboarding/cycles.webp diff --git a/apps/app/public/onboarding/issues.svg b/web/public/onboarding/issues.svg similarity index 100% rename from apps/app/public/onboarding/issues.svg rename to web/public/onboarding/issues.svg diff --git a/apps/app/public/onboarding/issues.webp b/web/public/onboarding/issues.webp similarity index 100% rename from apps/app/public/onboarding/issues.webp rename to web/public/onboarding/issues.webp diff --git a/apps/app/public/onboarding/modules.svg b/web/public/onboarding/modules.svg similarity index 100% rename from apps/app/public/onboarding/modules.svg rename to web/public/onboarding/modules.svg diff --git a/apps/app/public/onboarding/modules.webp b/web/public/onboarding/modules.webp similarity index 100% rename from apps/app/public/onboarding/modules.webp rename to web/public/onboarding/modules.webp diff --git a/apps/app/public/onboarding/pages.svg b/web/public/onboarding/pages.svg similarity index 100% rename from apps/app/public/onboarding/pages.svg rename to web/public/onboarding/pages.svg diff --git a/apps/app/public/onboarding/pages.webp b/web/public/onboarding/pages.webp similarity index 100% rename from apps/app/public/onboarding/pages.webp rename to web/public/onboarding/pages.webp diff --git a/apps/app/public/onboarding/views.svg b/web/public/onboarding/views.svg similarity index 100% rename from apps/app/public/onboarding/views.svg rename to web/public/onboarding/views.svg diff --git a/apps/app/public/onboarding/views.webp b/web/public/onboarding/views.webp similarity index 100% rename from apps/app/public/onboarding/views.webp rename to web/public/onboarding/views.webp diff --git a/web/public/plane-logos/black-horizontal-with-blue-logo.svg b/web/public/plane-logos/black-horizontal-with-blue-logo.svg new file mode 100644 index 00000000000..ae79919fc55 --- /dev/null +++ b/web/public/plane-logos/black-horizontal-with-blue-logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/web/public/plane-logos/blue-without-text.png b/web/public/plane-logos/blue-without-text.png new file mode 100644 index 00000000000..ea94aec7920 Binary files /dev/null and b/web/public/plane-logos/blue-without-text.png differ diff --git a/web/public/plane-logos/white-horizontal-with-blue-logo.svg b/web/public/plane-logos/white-horizontal-with-blue-logo.svg new file mode 100644 index 00000000000..1f09cc34ab6 --- /dev/null +++ b/web/public/plane-logos/white-horizontal-with-blue-logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/web/public/plane-logos/white-horizontal.svg b/web/public/plane-logos/white-horizontal.svg new file mode 100644 index 00000000000..13e2dbb9fa8 --- /dev/null +++ b/web/public/plane-logos/white-horizontal.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/app/public/services/csv.svg b/web/public/services/csv.svg similarity index 100% rename from apps/app/public/services/csv.svg rename to web/public/services/csv.svg diff --git a/apps/app/public/services/excel.svg b/web/public/services/excel.svg similarity index 100% rename from apps/app/public/services/excel.svg rename to web/public/services/excel.svg diff --git a/apps/app/public/services/github.png b/web/public/services/github.png similarity index 100% rename from apps/app/public/services/github.png rename to web/public/services/github.png diff --git a/apps/app/public/services/jira.png b/web/public/services/jira.png similarity index 100% rename from apps/app/public/services/jira.png rename to web/public/services/jira.png diff --git a/apps/app/public/services/json.svg b/web/public/services/json.svg similarity index 92% rename from apps/app/public/services/json.svg rename to web/public/services/json.svg index 4c2df222236..0fe32e276d4 100644 --- a/apps/app/public/services/json.svg +++ b/web/public/services/json.svg @@ -2,7 +2,7 @@ - + diff --git a/apps/app/public/services/slack.png b/web/public/services/slack.png similarity index 100% rename from apps/app/public/services/slack.png rename to web/public/services/slack.png diff --git a/apps/app/public/site.webmanifest.json b/web/public/site.webmanifest.json similarity index 100% rename from apps/app/public/site.webmanifest.json rename to web/public/site.webmanifest.json diff --git a/apps/app/public/sw.js b/web/public/sw.js similarity index 100% rename from apps/app/public/sw.js rename to web/public/sw.js diff --git a/apps/app/public/sw.js.map b/web/public/sw.js.map similarity index 100% rename from apps/app/public/sw.js.map rename to web/public/sw.js.map diff --git a/web/public/theme-mode/custom-mode.svg b/web/public/theme-mode/custom-mode.svg new file mode 100644 index 00000000000..01be18d5b40 --- /dev/null +++ b/web/public/theme-mode/custom-mode.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/theme-mode/custom-theme-banner.svg b/web/public/theme-mode/custom-theme-banner.svg new file mode 100644 index 00000000000..b7434cd764e --- /dev/null +++ b/web/public/theme-mode/custom-theme-banner.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/theme-mode/dark-high-contrast.svg b/web/public/theme-mode/dark-high-contrast.svg new file mode 100644 index 00000000000..e5839b28600 --- /dev/null +++ b/web/public/theme-mode/dark-high-contrast.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/theme-mode/dark-mode.svg b/web/public/theme-mode/dark-mode.svg new file mode 100644 index 00000000000..b8c14711c17 --- /dev/null +++ b/web/public/theme-mode/dark-mode.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/theme-mode/light-high-contrast.svg b/web/public/theme-mode/light-high-contrast.svg new file mode 100644 index 00000000000..c2f5ded22be --- /dev/null +++ b/web/public/theme-mode/light-high-contrast.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/theme-mode/light-mode.svg b/web/public/theme-mode/light-mode.svg new file mode 100644 index 00000000000..9c7fdae4b8d --- /dev/null +++ b/web/public/theme-mode/light-mode.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/app/public/user.png b/web/public/user.png similarity index 100% rename from apps/app/public/user.png rename to web/public/user.png diff --git a/web/public/web-view-spinner.png b/web/public/web-view-spinner.png new file mode 100644 index 00000000000..527f307c29f Binary files /dev/null and b/web/public/web-view-spinner.png differ diff --git a/apps/app/public/workbox-7805bd61.js b/web/public/workbox-7805bd61.js similarity index 100% rename from apps/app/public/workbox-7805bd61.js rename to web/public/workbox-7805bd61.js diff --git a/apps/app/public/workbox-7805bd61.js.map b/web/public/workbox-7805bd61.js.map similarity index 100% rename from apps/app/public/workbox-7805bd61.js.map rename to web/public/workbox-7805bd61.js.map diff --git a/apps/app/sentry.client.config.js b/web/sentry.client.config.js similarity index 100% rename from apps/app/sentry.client.config.js rename to web/sentry.client.config.js diff --git a/apps/app/sentry.edge.config.js b/web/sentry.edge.config.js similarity index 100% rename from apps/app/sentry.edge.config.js rename to web/sentry.edge.config.js diff --git a/apps/app/sentry.properties b/web/sentry.properties similarity index 100% rename from apps/app/sentry.properties rename to web/sentry.properties diff --git a/apps/app/sentry.server.config.js b/web/sentry.server.config.js similarity index 100% rename from apps/app/sentry.server.config.js rename to web/sentry.server.config.js diff --git a/web/services/ai.service.ts b/web/services/ai.service.ts new file mode 100644 index 00000000000..63bf02ee439 --- /dev/null +++ b/web/services/ai.service.ts @@ -0,0 +1,30 @@ +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import { IUser, IGptResponse } from "types"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class AIService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async createGptTask( + workspaceSlug: string, + projectId: string, + data: { prompt: string; task: string }, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data) + .then((response) => { + trackEventService.trackAskGptEvent(response?.data, "ASK_GPT", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response; + }); + } +} diff --git a/apps/app/services/analytics.service.ts b/web/services/analytics.service.ts similarity index 86% rename from apps/app/services/analytics.service.ts rename to web/services/analytics.service.ts index 0b38f8c570d..38d0ab77492 100644 --- a/apps/app/services/analytics.service.ts +++ b/web/services/analytics.service.ts @@ -1,5 +1,5 @@ // services -import APIService from "services/api.service"; +import { APIService } from "services/api.service"; // types import { IAnalyticsParams, @@ -8,12 +8,12 @@ import { IExportAnalyticsFormData, ISaveAnalyticsFormData, } from "types"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; -const { NEXT_PUBLIC_API_BASE_URL } = process.env; - -class AnalyticsServices extends APIService { +export class AnalyticsService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise { @@ -61,5 +61,3 @@ class AnalyticsServices extends APIService { }); } } - -export default new AnalyticsServices(); diff --git a/web/services/api.service.ts b/web/services/api.service.ts new file mode 100644 index 00000000000..621bac767e4 --- /dev/null +++ b/web/services/api.service.ts @@ -0,0 +1,94 @@ +import axios from "axios"; +import Cookies from "js-cookie"; + +export abstract class APIService { + protected baseURL: string; + protected headers: any = {}; + + constructor(baseURL: string) { + this.baseURL = baseURL; + } + + setRefreshToken(token: string) { + Cookies.set("refreshToken", token); + } + + getRefreshToken() { + return Cookies.get("refreshToken"); + } + + purgeRefreshToken() { + Cookies.remove("refreshToken", { path: "/" }); + } + + setAccessToken(token: string) { + Cookies.set("accessToken", token); + } + + getAccessToken() { + return Cookies.get("accessToken"); + } + + purgeAccessToken() { + Cookies.remove("accessToken", { path: "/" }); + } + + getHeaders() { + return { + Authorization: `Bearer ${this.getAccessToken()}`, + }; + } + + get(url: string, config = {}): Promise { + return axios({ + method: "get", + url: this.baseURL + url, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + post(url: string, data = {}, config = {}): Promise { + return axios({ + method: "post", + url: this.baseURL + url, + data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + put(url: string, data = {}, config = {}): Promise { + return axios({ + method: "put", + url: this.baseURL + url, + data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + patch(url: string, data = {}, config = {}): Promise { + return axios({ + method: "patch", + url: this.baseURL + url, + data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + delete(url: string, data?: any, config = {}): Promise { + return axios({ + method: "delete", + url: this.baseURL + url, + data: data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + request(config = {}) { + return axios(config); + } +} diff --git a/web/services/app_config.service.ts b/web/services/app_config.service.ts new file mode 100644 index 00000000000..5843c01c96e --- /dev/null +++ b/web/services/app_config.service.ts @@ -0,0 +1,30 @@ +// services +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; +} + +export class AppConfigService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async envConfig(): Promise { + return this.get("/api/configs/", { + headers: { + "Content-Type": "application/json", + }, + }) + .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 new file mode 100644 index 00000000000..17972103640 --- /dev/null +++ b/web/services/app_installation.service.ts @@ -0,0 +1,63 @@ +// services +import { APIService } from "services/api.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +export class AppInstallationService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async addInstallationApp(workspaceSlug: string, provider: string, data: any): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/workspace-integrations/${provider}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async addSlackChannel( + workspaceSlug: string, + projectId: string, + integrationId: string | null | undefined, + data: any + ): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${integrationId}/project-slack-sync/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async getSlackChannelDetail( + workspaceSlug: string, + projectId: string, + integrationId: string | null | undefined + ): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${integrationId}/project-slack-sync/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async removeSlackChannel( + workspaceSlug: string, + projectId: string, + integrationId: string | null | undefined, + slackSyncId: string | undefined + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${integrationId}/project-slack-sync/${slackSyncId}` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} diff --git a/web/services/auth.service.ts b/web/services/auth.service.ts new file mode 100644 index 00000000000..f7dd9e94c04 --- /dev/null +++ b/web/services/auth.service.ts @@ -0,0 +1,83 @@ +// services +import { APIService } from "services/api.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +export interface ILoginTokenResponse { + access_token: string; + refresh_toke: string; +} + +export class AuthService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async emailLogin(data: any): Promise { + return this.post("/api/sign-in/", data, { headers: {} }) + .then((response) => { + this.setAccessToken(response?.data?.access_token); + this.setRefreshToken(response?.data?.refresh_token); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async emailSignUp(data: { email: string; password: string }): Promise { + return this.post("/api/sign-up/", data, { headers: {} }) + .then((response) => { + this.setAccessToken(response?.data?.access_token); + this.setRefreshToken(response?.data?.refresh_token); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async socialAuth(data: any): Promise { + return this.post("/api/social-auth/", data, { headers: {} }) + .then((response) => { + this.setAccessToken(response?.data?.access_token); + this.setRefreshToken(response?.data?.refresh_token); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async emailCode(data: any): Promise { + return this.post("/api/magic-generate/", data, { headers: {} }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async magicSignIn(data: any): Promise { + const response = await this.post("/api/magic-sign-in/", data, { headers: {} }); + if (response?.status === 200) { + this.setAccessToken(response?.data?.access_token); + this.setRefreshToken(response?.data?.refresh_token); + return response?.data; + } + throw response.response.data; + } + + async signOut(): Promise { + return this.post("/api/sign-out/", { refresh_token: this.getRefreshToken() }) + .then((response) => { + this.purgeAccessToken(); + this.purgeRefreshToken(); + return response?.data; + }) + .catch((error) => { + this.purgeAccessToken(); + this.purgeRefreshToken(); + throw error?.response?.data; + }); + } +} diff --git a/web/services/cycle.service.ts b/web/services/cycle.service.ts new file mode 100644 index 00000000000..65188515ccd --- /dev/null +++ b/web/services/cycle.service.ts @@ -0,0 +1,163 @@ +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import type { CycleDateCheckData, IUser, ICycle, IIssue } from "types"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class CycleService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async createCycle(workspaceSlug: string, projectId: string, data: any, user: any): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) + .then((response) => { + trackEventService.trackCycleEvent(response?.data, "CYCLE_CREATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getCyclesWithParams( + workspaceSlug: string, + projectId: string, + cycleType: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, { + params: { + cycle_view: cycleType, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getCycleDetails(workspaceSlug: string, projectId: string, cycleId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }); + } + + async getCycleIssues(workspaceSlug: string, projectId: string, cycleId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getCycleIssuesWithParams( + workspaceSlug: string, + projectId: string, + cycleId: string, + queries?: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + 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, + cycleId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.patch(`/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 deleteCycle(workspaceSlug: string, projectId: string, cycleId: string, user: IUser | undefined): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`) + .then((response) => { + trackEventService.trackCycleEvent(response?.data, "CYCLE_DELETE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async cycleDateCheck(workspaceSlug: string, projectId: string, data: CycleDateCheckData): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async addCycleToFavorites( + workspaceSlug: string, + projectId: string, + data: { + cycle: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async transferIssues( + workspaceSlug: string, + projectId: string, + cycleId: string, + data: { + new_cycle_id: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/transfer-issues/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async removeCycleFromFavorites(workspaceSlug: string, projectId: string, cycleId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-cycles/${cycleId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/file.service.ts b/web/services/file.service.ts new file mode 100644 index 00000000000..0e3749a4cd8 --- /dev/null +++ b/web/services/file.service.ts @@ -0,0 +1,123 @@ +// services +import { APIService } from "services/api.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +export interface UnSplashImage { + id: string; + created_at: Date; + updated_at: Date; + promoted_at: Date; + width: number; + height: number; + color: string; + blur_hash: string; + description: null; + alt_description: string; + urls: UnSplashImageUrls; + [key: string]: any; +} + +export interface UnSplashImageUrls { + raw: string; + full: string; + regular: string; + small: string; + thumb: string; + small_s3: string; +} + +export class FileService extends APIService { + constructor() { + super(API_BASE_URL); + this.uploadFile = this.uploadFile.bind(this); + this.deleteImage = this.deleteImage.bind(this); + } + + async uploadFile(workspaceSlug: string, file: FormData): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, { + headers: { + ...this.getHeaders(), + "Content-Type": "multipart/form-data", + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + getUploadFileFunction(workspaceSlug: string): (file: File) => Promise { + return async (file: File) => { + const formData = new FormData(); + formData.append("asset", file); + formData.append("attributes", JSON.stringify({})); + + const data = await this.uploadFile(workspaceSlug, formData); + return data.asset; + }; + } + + async deleteImage(assetUrlWithWorkspaceId: string): Promise { + return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`) + .then((response) => response?.status) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteFile(workspaceId: string, assetUrl: string): Promise { + const lastIndex = assetUrl.lastIndexOf("/"); + const assetId = assetUrl.substring(lastIndex + 1); + + return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async uploadUserFile(file: FormData): Promise { + return this.post(`/api/users/file-assets/`, file, { + headers: { + ...this.getHeaders(), + "Content-Type": "multipart/form-data", + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteUserFile(assetUrl: string): Promise { + const lastIndex = assetUrl.lastIndexOf("/"); + const assetId = assetUrl.substring(lastIndex + 1); + + return this.delete(`/api/users/file-assets/${assetId}`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUnsplashImages(query?: string): Promise { + return this.get(`/api/unsplash/`, { + params: { + query, + }, + }) + .then((res) => res?.data?.results ?? res?.data) + .catch((err) => { + throw err?.response?.data; + }); + } + + async getProjectCoverImages(): Promise { + return this.get(`/api/project-covers/`) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }); + } +} diff --git a/web/services/inbox.service.ts b/web/services/inbox.service.ts new file mode 100644 index 00000000000..05b19aa415b --- /dev/null +++ b/web/services/inbox.service.ts @@ -0,0 +1,154 @@ +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; +// types +import type { IInboxIssue, IInbox, TInboxStatus, IUser, IInboxQueryParams } from "types"; + +const trackEventService = new TrackEventService(); + +export class InboxService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getInboxes(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getInboxById(workspaceSlug: string, projectId: string, inboxId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchInbox(workspaceSlug: string, projectId: string, inboxId: string, data: Partial): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getInboxIssues( + workspaceSlug: string, + projectId: string, + inboxId: string, + params?: IInboxQueryParams + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getInboxIssueById( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string + ): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteInboxIssue( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string, + user: IUser | undefined + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/` + ) + .then((response) => { + if (user) trackEventService.trackInboxEvent(response?.data, "INBOX_ISSUE_DELETE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markInboxStatus( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string, + data: TInboxStatus, + user: IUser | undefined + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`, + data + ) + .then((response) => { + const action = + data.status === -1 + ? "INBOX_ISSUE_REJECTED" + : data.status === 0 + ? "INBOX_ISSUE_SNOOZED" + : data.status === 1 + ? "INBOX_ISSUE_ACCEPTED" + : "INBOX_ISSUE_DUPLICATED"; + trackEventService.trackInboxEvent(response?.data, action, user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchInboxIssue( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string, + data: { issue: Partial }, + user: IUser | undefined + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`, + data + ) + .then((response) => { + if (user) trackEventService.trackInboxEvent(response?.data, "INBOX_ISSUE_UPDATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createInboxIssue( + workspaceSlug: string, + projectId: string, + inboxId: string, + data: any, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, data) + .then((response) => { + if (user) trackEventService.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/integrations/github.service.ts b/web/services/integrations/github.service.ts new file mode 100644 index 00000000000..bec6101e5ad --- /dev/null +++ b/web/services/integrations/github.service.ts @@ -0,0 +1,49 @@ +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; +// types +import { IUser, IGithubRepoInfo, IGithubServiceImportFormData } from "types"; + +const integrationServiceType: string = "github"; + +const trackEventService = new TrackEventService(); + +export class GithubIntegrationService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async listAllRepositories(workspaceSlug: string, integrationSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/workspace-integrations/${integrationSlug}/github-repositories`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getGithubRepoInfo(workspaceSlug: string, params: { owner: string; repo: string }): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/importers/${integrationServiceType}/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createGithubServiceImport( + workspaceSlug: string, + data: IGithubServiceImportFormData, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/${integrationServiceType}/`, data) + .then((response) => { + trackEventService.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/integrations/index.ts b/web/services/integrations/index.ts new file mode 100644 index 00000000000..cf9a070eec3 --- /dev/null +++ b/web/services/integrations/index.ts @@ -0,0 +1,3 @@ +export * from "./github.service"; +export * from "./integration.service"; +export * from "./jira.service"; diff --git a/web/services/integrations/integration.service.ts b/web/services/integrations/integration.service.ts new file mode 100644 index 00000000000..e36ea4889eb --- /dev/null +++ b/web/services/integrations/integration.service.ts @@ -0,0 +1,79 @@ +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import { IAppIntegration, IUser, IImporterService, IWorkspaceIntegration, IExportServiceResponse } from "types"; +// helper +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class IntegrationService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getAppIntegrationsList(): Promise { + return this.get(`/api/integrations/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getWorkspaceIntegrationsList(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/workspace-integrations/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteWorkspaceIntegration(workspaceSlug: string, integrationId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/workspace-integrations/${integrationId}/provider/`) + .then((res) => res?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getImporterServicesList(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/importers/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async getExportsServicesList( + workspaceSlug: string, + cursor: string, + per_page: number + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/export-issues`, { + params: { + per_page, + cursor, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteImporterService( + workspaceSlug: string, + service: string, + importerId: string, + user: IUser | undefined + ): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`) + .then((response) => { + const eventName = service === "github" ? "GITHUB_IMPORTER_DELETE" : "JIRA_IMPORTER_DELETE"; + trackEventService.trackImporterEvent(response?.data, eventName, user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/integrations/jira.service.ts b/web/services/integrations/jira.service.ts new file mode 100644 index 00000000000..744dc2b115f --- /dev/null +++ b/web/services/integrations/jira.service.ts @@ -0,0 +1,38 @@ +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +import { API_BASE_URL } from "helpers/common.helper"; +// types +import { IJiraMetadata, IJiraResponse, IJiraImporterForm, IUser } from "types"; + +const trackEventService = new TrackEventService(); + +export class JiraImporterService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getJiraProjectInfo(workspaceSlug: string, params: IJiraMetadata): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/importers/jira`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createJiraImporter( + workspaceSlug: string, + data: IJiraImporterForm, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data) + .then((response) => { + trackEventService.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/issue/index.ts b/web/services/issue/index.ts new file mode 100644 index 00000000000..2f92d64290f --- /dev/null +++ b/web/services/issue/index.ts @@ -0,0 +1,7 @@ +export * from "./issue_archive.service"; +export * from "./issue.service"; +export * from "./issue_draft.service"; +export * from "./issue_reaction.service"; +export * from "./issue_label.service"; +export * from "./issue_attachment.service"; +export * from "./issue_comment.service"; diff --git a/web/services/issue/issue.service.ts b/web/services/issue/issue.service.ts new file mode 100644 index 00000000000..5f284489631 --- /dev/null +++ b/web/services/issue/issue.service.ts @@ -0,0 +1,277 @@ +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// type +import type { IUser, IIssue, IIssueActivity, ISubIssueResponse, IIssueDisplayProperties } from "types"; +// helper +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class IssueService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async createIssue(workspaceSlug: string, projectId: string, data: any, user: IUser | undefined): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, data) + .then((response) => { + trackEventService.trackIssueEvent(response.data, "ISSUE_CREATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getIssues(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getIssuesWithParams( + workspaceSlug: string, + projectId: string, + queries?: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async retrieve(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getIssueActivities(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async addIssueToCycle( + workspaceSlug: string, + projectId: string, + cycleId: string, + data: { + issues: string[]; + }, + user: IUser | undefined + ) { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`, data) + .then((response) => { + trackEventService.trackIssueMovedToCycleOrModuleEvent( + { + workspaceSlug, + workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name, + projectId, + projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier, + projectName: response?.data?.[0]?.issue_detail?.project_detail?.name, + issueId: response?.data?.[0]?.issue_detail?.id, + cycleId, + }, + response.data.length > 1 ? "ISSUE_MOVED_TO_CYCLE_IN_BULK" : "ISSUE_MOVED_TO_CYCLE", + user as IUser + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async removeIssueFromCycle(workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/${bridgeId}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createIssueRelation( + workspaceSlug: string, + projectId: string, + issueId: string, + user: IUser, + data: { + related_list: Array<{ + relation_type: "duplicate" | "relates_to" | "blocked_by"; + related_issue: string; + }>; + relation?: "blocking" | null; + } + ) { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/`, data) + .then((response) => { + trackEventService.trackIssueRelationEvent(response.data, "ISSUE_RELATION_CREATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response; + }); + } + + async deleteIssueRelation( + workspaceSlug: string, + projectId: string, + issueId: string, + relationId: string, + user: IUser + ) { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/${relationId}/` + ) + .then((response) => { + trackEventService.trackIssueRelationEvent(response.data, "ISSUE_RELATION_DELETE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response; + }); + } + + async getIssueDisplayProperties(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-display-properties/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateIssueDisplayProperties( + workspaceSlug: string, + projectId: string, + data: IIssueDisplayProperties + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-display-properties/`, { + properties: data, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchIssue( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, data) + .then((response) => { + trackEventService.trackIssueEvent(response.data, "ISSUE_UPDATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string, user: IUser | undefined): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`) + .then((response) => { + trackEventService.trackIssueEvent({ issuesId }, "ISSUE_DELETE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async bulkDeleteIssues(workspaceSlug: string, projectId: string, data: any, user: IUser | undefined): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-delete-issues/`, data) + .then((response) => { + trackEventService.trackIssueBulkDeleteEvent(data, user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async subIssues(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async addSubIssues( + workspaceSlug: string, + projectId: string, + issueId: string, + data: { sub_issue_ids: string[] } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createIssueLink( + workspaceSlug: string, + projectId: string, + issueId: string, + data: { + metadata: any; + title: string; + url: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async updateIssueLink( + workspaceSlug: string, + projectId: string, + issueId: string, + linkId: string, + data: { + metadata: any; + title: string; + url: string; + } + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async deleteIssueLink(workspaceSlug: string, projectId: string, issueId: string, linkId: string): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/issue/issue_archive.service.ts b/web/services/issue/issue_archive.service.ts new file mode 100644 index 00000000000..02cb6357ebf --- /dev/null +++ b/web/services/issue/issue_archive.service.ts @@ -0,0 +1,43 @@ +import { APIService } from "services/api.service"; +// type +import { API_BASE_URL } from "helpers/common.helper"; + +export class IssueArchiveService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getArchivedIssues(workspaceSlug: string, projectId: string, queries?: any): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async unarchiveIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/unarchive/${issueId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async retrieveArchivedIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issueId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteArchivedIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issuesId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/issue/issue_attachment.service.ts b/web/services/issue/issue_attachment.service.ts new file mode 100644 index 00000000000..94c94ded168 --- /dev/null +++ b/web/services/issue/issue_attachment.service.ts @@ -0,0 +1,49 @@ +import { APIService } from "services/api.service"; +// helper +import { API_BASE_URL } from "helpers/common.helper"; + +export class IssueAttachmentService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async uploadIssueAttachment(workspaceSlug: string, projectId: string, issueId: string, file: FormData): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-attachments/`, + file, + { + headers: { + ...this.getHeaders(), + "Content-Type": "multipart/form-data", + }, + } + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getIssueAttachment(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-attachments/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteIssueAttachment( + workspaceSlug: string, + projectId: string, + issueId: string, + assetId: string + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-attachments/${assetId}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/issue/issue_comment.service.ts b/web/services/issue/issue_comment.service.ts new file mode 100644 index 00000000000..12c08a517cd --- /dev/null +++ b/web/services/issue/issue_comment.service.ts @@ -0,0 +1,86 @@ +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import { IIssueComment, IUser } from "types"; +// helper +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class IssueCommentService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getIssueComments(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createIssueComment( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`, data) + .then((response) => { + trackEventService.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchIssueComment( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`, + data + ) + .then((response) => { + trackEventService.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteIssueComment( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + user: IUser | undefined + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/` + ) + .then((response) => { + trackEventService.trackIssueCommentEvent( + { + issueId, + commentId, + }, + "ISSUE_COMMENT_DELETE", + user + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/issue/issue_draft.service.tsx b/web/services/issue/issue_draft.service.tsx new file mode 100644 index 00000000000..b329febfb42 --- /dev/null +++ b/web/services/issue/issue_draft.service.tsx @@ -0,0 +1,51 @@ +import { APIService } from "services/api.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +export class IssueDraftService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getDraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async createDraftIssue(workspaceSlug: string, projectId: string, data: any): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async updateDraftIssue(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async deleteDraftIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} diff --git a/web/services/issue/issue_label.service.ts b/web/services/issue/issue_label.service.ts new file mode 100644 index 00000000000..00b945d018a --- /dev/null +++ b/web/services/issue/issue_label.service.ts @@ -0,0 +1,112 @@ +import { API_BASE_URL } from "helpers/common.helper"; +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import { IIssueLabels, IUser } from "types"; + +const trackEventServices = new TrackEventService(); + +export class IssueLabelService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getWorkspaceIssueLabels(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/labels/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getProjectIssueLabels(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createIssueLabel( + workspaceSlug: string, + projectId: string, + data: any, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`, data) + .then((response: { data: IIssueLabels; [key: string]: any }) => { + trackEventServices.trackIssueLabelEvent( + { + workSpaceId: response?.data?.workspace_detail?.id, + workSpaceName: response?.data?.workspace_detail?.name, + workspaceSlug, + projectId, + projectIdentifier: response?.data?.project_detail?.identifier, + projectName: response?.data?.project_detail?.name, + labelId: response?.data?.id, + color: response?.data?.color, + }, + "ISSUE_LABEL_CREATE", + user as IUser + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchIssueLabel( + workspaceSlug: string, + projectId: string, + labelId: string, + data: any, + user: IUser | undefined + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`, data) + .then((response) => { + trackEventServices.trackIssueLabelEvent( + { + workSpaceId: response?.data?.workspace_detail?.id, + workSpaceName: response?.data?.workspace_detail?.name, + workspaceSlug, + projectId, + projectIdentifier: response?.data?.project_detail?.identifier, + projectName: response?.data?.project_detail?.name, + labelId: response?.data?.id, + color: response?.data?.color, + }, + "ISSUE_LABEL_UPDATE", + user as IUser + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteIssueLabel( + workspaceSlug: string, + projectId: string, + labelId: string, + user: IUser | undefined + ): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`) + .then((response) => { + trackEventServices.trackIssueLabelEvent( + { + workspaceSlug, + projectId, + }, + "ISSUE_LABEL_DELETE", + user as IUser + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/issue/issue_reaction.service.ts b/web/services/issue/issue_reaction.service.ts new file mode 100644 index 00000000000..737d9879f2c --- /dev/null +++ b/web/services/issue/issue_reaction.service.ts @@ -0,0 +1,106 @@ +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, IssueReaction, IssueCommentReaction, IssueReactionForm, IssueCommentReactionForm } from "types"; + +const trackEventService = new TrackEventService(); + +export class IssueReactionService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async createIssueReaction( + workspaceSlug: string, + projectId: string, + issueId: string, + data: IssueReactionForm, + user?: IUser + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/`, data) + .then((response) => { + trackEventService.trackReactionEvent(response?.data, "ISSUE_REACTION_CREATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async listIssueReactions(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteIssueReaction( + workspaceSlug: string, + projectId: string, + issueId: string, + reaction: string, + user?: IUser + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/${reaction}/` + ) + .then((response) => { + trackEventService.trackReactionEvent(response?.data, "ISSUE_REACTION_DELETE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createIssueCommentReaction( + workspaceSlug: string, + projectId: string, + commentId: string, + data: IssueCommentReactionForm, + user?: IUser + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/`, data) + .then((response) => { + trackEventService.trackReactionEvent(response?.data, "ISSUE_COMMENT_REACTION_CREATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async listIssueCommentReactions( + workspaceSlug: string, + projectId: string, + commentId: string + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteIssueCommentReaction( + workspaceSlug: string, + projectId: string, + commentId: string, + reaction: string, + user?: IUser + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/${reaction}/` + ) + .then((response) => { + trackEventService.trackReactionEvent(response?.data, "ISSUE_COMMENT_REACTION_DELETE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/module.service.ts b/web/services/module.service.ts new file mode 100644 index 00000000000..dec4b2d8ff9 --- /dev/null +++ b/web/services/module.service.ts @@ -0,0 +1,228 @@ +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import type { IModule, IIssue, IUser } from "types"; +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class ModuleService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getModules(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createModule(workspaceSlug: string, projectId: string, data: any, user: any): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data) + .then((response) => { + trackEventService.trackModuleEvent(response?.data, "MODULE_CREATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateModule( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: any, + user: IUser | undefined + ): Promise { + return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data) + .then((response) => { + trackEventService.trackModuleEvent(response?.data, "MODULE_UPDATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getModuleDetails(workspaceSlug: string, projectId: string, moduleId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchModule( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data) + .then((response) => { + if (user) trackEventService.trackModuleEvent(response?.data, "MODULE_UPDATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteModule(workspaceSlug: string, projectId: string, moduleId: string, user: any): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`) + .then((response) => { + trackEventService.trackModuleEvent(response?.data, "MODULE_DELETE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getModuleIssues(workspaceSlug: string, projectId: string, moduleId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getModuleIssuesWithParams( + workspaceSlug: string, + projectId: string, + moduleId: string, + queries?: any + ): Promise< + | IIssue[] + | { + [key: string]: IIssue[]; + } + > { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async addIssuesToModule( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: { issues: string[] }, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, data) + .then((response) => { + trackEventService.trackIssueMovedToCycleOrModuleEvent( + { + workspaceSlug, + workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name, + projectId, + projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier, + projectName: response?.data?.[0]?.issue_detail?.project_detail?.name, + issueId: response?.data?.[0]?.issue_detail?.id, + moduleId, + }, + response?.data?.length > 1 ? "ISSUE_MOVED_TO_MODULE_IN_BULK" : "ISSUE_MOVED_TO_MODULE", + user as IUser + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async removeIssueFromModule( + workspaceSlug: string, + projectId: string, + moduleId: string, + bridgeId: string + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/${bridgeId}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createModuleLink( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: { + metadata: any; + title: string; + url: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async updateModuleLink( + workspaceSlug: string, + projectId: string, + moduleId: string, + linkId: string, + data: { + metadata: any; + title: string; + url: string; + } + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/${linkId}/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async deleteModuleLink(workspaceSlug: string, projectId: string, moduleId: string, linkId: string): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-links/${linkId}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async addModuleToFavorites( + workspaceSlug: string, + projectId: string, + data: { + module: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async removeModuleFromFavorites(workspaceSlug: string, projectId: string, moduleId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-modules/${moduleId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/notification.service.ts b/web/services/notification.service.ts new file mode 100644 index 00000000000..fe8a24b6c95 --- /dev/null +++ b/web/services/notification.service.ts @@ -0,0 +1,144 @@ +// services +import { APIService } from "services/api.service"; +// types +import type { + IUserNotification, + INotificationParams, + NotificationCount, + PaginatedUserNotification, + IMarkAllAsReadPayload, +} from "types"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +export class NotificationService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getUserNotifications(workspaceSlug: string, params: INotificationParams): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/users/notifications`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUserNotificationDetailById(workspaceSlug: string, notificationId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markUserNotificationAsRead(workspaceSlug: string, notificationId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markUserNotificationAsUnread(workspaceSlug: string, notificationId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markUserNotificationAsArchived(workspaceSlug: string, notificationId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markUserNotificationAsUnarchived(workspaceSlug: string, notificationId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchUserNotification( + workspaceSlug: string, + notificationId: string, + data: Partial + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteUserNotification(workspaceSlug: string, notificationId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async subscribeToIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getIssueNotificationSubscriptionStatus( + workspaceSlug: string, + projectId: string, + issueId: string + ): Promise<{ + subscribed: boolean; + }> { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async unsubscribeFromIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUnreadNotificationsCount(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/unread/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getNotifications(url: string): Promise { + return this.get(url) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markAllNotificationsAsRead(workspaceSlug: string, payload: IMarkAllAsReadPayload): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/mark-all-read/`, { + ...payload, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/page.service.ts b/web/services/page.service.ts new file mode 100644 index 00000000000..a344c69ecd8 --- /dev/null +++ b/web/services/page.service.ts @@ -0,0 +1,215 @@ +import { API_BASE_URL } from "helpers/common.helper"; +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import { IPage, IPageBlock, RecentPagesResponse, IIssue, IUser } from "types"; + +const trackEventService = new TrackEventService(); + +export class PageService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async createPage( + workspaceSlug: string, + projectId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, data) + .then((response) => { + trackEventService.trackPageEvent(response?.data, "PAGE_CREATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchPage( + workspaceSlug: string, + projectId: string, + pageId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`, data) + .then((response) => { + trackEventService.trackPageEvent(response?.data, "PAGE_UPDATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deletePage(workspaceSlug: string, projectId: string, pageId: string, user: IUser | undefined): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`) + .then((response) => { + trackEventService.trackPageEvent(response?.data, "PAGE_DELETE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async addPageToFavorites( + workspaceSlug: string, + projectId: string, + data: { + page: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-pages/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async removePageFromFavorites(workspaceSlug: string, projectId: string, pageId: string) { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-pages/${pageId}`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getPagesWithParams( + workspaceSlug: string, + projectId: string, + pageType: "all" | "favorite" | "created_by_me" | "created_by_other" + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, { + params: { + page_view: pageType, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getRecentPages(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, { + params: { + page_view: "recent", + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getPageDetails(workspaceSlug: string, projectId: string, pageId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createPageBlock( + workspaceSlug: string, + projectId: string, + pageId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/`, data) + .then((response) => { + trackEventService.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CREATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getPageBlock( + workspaceSlug: string, + projectId: string, + pageId: string, + pageBlockId: string + ): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchPageBlock( + workspaceSlug: string, + projectId: string, + pageId: string, + pageBlockId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/`, + data + ) + .then((response) => { + trackEventService.trackPageBlockEvent(response?.data, "PAGE_BLOCK_UPDATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deletePageBlock( + workspaceSlug: string, + projectId: string, + pageId: string, + pageBlockId: string, + user: IUser | undefined + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/` + ) + .then((response) => { + trackEventService.trackPageBlockEvent(response?.data, "PAGE_BLOCK_DELETE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async listPageBlocks(workspaceSlug: string, projectId: string, pageId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async convertPageBlockToIssue( + workspaceSlug: string, + projectId: string, + pageId: string, + blockId: string, + user: IUser | undefined + ): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${blockId}/issues/` + ) + .then((response) => { + trackEventService.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CONVERTED_TO_ISSUE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/project/index.ts b/web/services/project/index.ts new file mode 100644 index 00000000000..538b65e1182 --- /dev/null +++ b/web/services/project/index.ts @@ -0,0 +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"; diff --git a/web/services/project/project.service.ts b/web/services/project/project.service.ts new file mode 100644 index 00000000000..1c824a40c10 --- /dev/null +++ b/web/services/project/project.service.ts @@ -0,0 +1,277 @@ +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 { + GithubRepositoriesResponse, + IUser, + IProject, + IProjectBulkAddFormData, + IProjectMember, + ISearchIssueResponse, + ProjectPreferences, + IProjectViewProps, + TProjectIssuesSearchParams, +} from "types"; + +const trackEventService = new TrackEventService(); + +export class ProjectService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async createProject(workspaceSlug: string, data: Partial, user: any): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data) + .then((response) => { + trackEventService.trackProjectEvent(response.data, "CREATE_PROJECT", user); + return response?.data; + }) + .catch((error) => { + throw error?.response; + }); + } + + async checkProjectIdentifierAvailability(workspaceSlug: string, data: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/project-identifiers`, { + params: { + name: data, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getProjects(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getProject(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateProject(workspaceSlug: string, projectId: string, data: Partial, user: any): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data) + .then((response) => { + trackEventService.trackProjectEvent(response.data, "UPDATE_PROJECT", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteProject(workspaceSlug: string, projectId: string, user: any | undefined): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`) + .then((response) => { + trackEventService.trackProjectEvent({ projectId }, "DELETE_PROJECT", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async joinProject(workspaceSlug: string, project_ids: string[]): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/join/`, { project_ids }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async leaveProject(workspaceSlug: string, projectId: string, user: any): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/leave/`) + .then((response) => { + trackEventService.trackProjectEvent( + "PROJECT_MEMBER_LEAVE", + { + workspaceSlug, + projectId, + ...response?.data, + }, + user + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + 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, + data: { + view_props?: IProjectViewProps; + default_props?: IProjectViewProps; + preferences?: ProjectPreferences; + sort_order?: number; + } + ): Promise { + await this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getGithubRepositories(url: string): Promise { + return this.request(url) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async syncGithubRepository( + workspaceSlug: string, + projectId: string, + workspaceIntegrationId: string, + data: { + name: string; + owner: string; + repository_id: string; + url: string; + } + ): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${workspaceIntegrationId}/github-repository-sync/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getProjectGithubRepository(workspaceSlug: string, projectId: string, integrationId: string): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/workspace-integrations/${integrationId}/github-repository-sync/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUserProjectFavorites(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/user-favorite-projects/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async addProjectToFavorites(workspaceSlug: string, project: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/user-favorite-projects/`, { project }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async removeProjectFromFavorites(workspaceSlug: string, projectId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/user-favorite-projects/${projectId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async projectIssuesSearch( + workspaceSlug: string, + projectId: string, + params: TProjectIssuesSearchParams + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/search-issues/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/project/project_estimate.service.ts b/web/services/project/project_estimate.service.ts new file mode 100644 index 00000000000..322d986fa9b --- /dev/null +++ b/web/services/project/project_estimate.service.ts @@ -0,0 +1,80 @@ +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import type { IUser, IEstimate, IEstimateFormData } from "types"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class ProjectEstimateService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async createEstimate( + workspaceSlug: string, + projectId: string, + data: IEstimateFormData, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data) + .then((response) => { + trackEventService.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response; + }); + } + + async patchEstimate( + workspaceSlug: string, + projectId: string, + estimateId: string, + data: IEstimateFormData, + user: IUser | undefined + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`, data) + .then((response) => { + trackEventService.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getEstimateDetails(workspaceSlug: string, projectId: string, estimateId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getEstimatesList(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteEstimate( + workspaceSlug: string, + projectId: string, + estimateId: string, + user: IUser | undefined + ): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`) + .then((response) => { + trackEventService.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/project/project_export.service.ts b/web/services/project/project_export.service.ts new file mode 100644 index 00000000000..64e42925c0a --- /dev/null +++ b/web/services/project/project_export.service.ts @@ -0,0 +1,38 @@ +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import { IUser } from "types"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class ProjectExportService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async csvExport( + workspaceSlug: string, + data: { + provider: string; + project: string[]; + }, + user: IUser + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/export-issues/`, data) + .then((response) => { + trackEventService.trackExporterEvent( + { + workspaceSlug, + }, + "CSV_EXPORTER_CREATE", + user + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/project/project_invitation.service.ts b/web/services/project/project_invitation.service.ts new file mode 100644 index 00000000000..1fbf6e24c24 --- /dev/null +++ b/web/services/project/project_invitation.service.ts @@ -0,0 +1,35 @@ +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/services/project/project_publish.service.ts b/web/services/project/project_publish.service.ts new file mode 100644 index 00000000000..16d705b4573 --- /dev/null +++ b/web/services/project/project_publish.service.ts @@ -0,0 +1,61 @@ +import { API_BASE_URL } from "helpers/common.helper"; +// services +import { APIService } from "services/api.service"; +// types +import { IProjectPublishSettings } from "store/project"; + +export class ProjectPublishService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getProjectSettingsAsync(workspace_slug: string, project_slug: string): Promise { + return this.get(`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async createProjectSettingsAsync( + workspace_slug: string, + project_slug: string, + data: IProjectPublishSettings + ): Promise { + return this.post(`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async updateProjectSettingsAsync( + workspace_slug: string, + project_slug: string, + project_publish_id: string, + data: IProjectPublishSettings + ): Promise { + return this.patch( + `/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/${project_publish_id}/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async deleteProjectSettingsAsync( + workspace_slug: string, + project_slug: string, + project_publish_id: string + ): Promise { + return this.delete( + `/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/${project_publish_id}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} diff --git a/web/services/project/project_state.service.ts b/web/services/project/project_state.service.ts new file mode 100644 index 00000000000..018df3a2053 --- /dev/null +++ b/web/services/project/project_state.service.ts @@ -0,0 +1,96 @@ +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; +// types +import type { IUser, IState, IStateResponse } from "types"; + +const trackEventService = new TrackEventService(); + +export class ProjectStateService extends APIService { + constructor() { + super(API_BASE_URL); + } + + 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); + return response?.data; + }) + .catch((error) => { + throw error?.response; + }); + } + + 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; + }); + } + + async getIssuesByState(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/?group_by=state`) + + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getState(workspaceSlug: string, projectId: string, stateId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateState( + workspaceSlug: string, + projectId: string, + stateId: string, + data: IState, + user: IUser | undefined + ): Promise { + return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`, data) + .then((response) => { + trackEventService.trackStateEvent(response?.data, "STATE_UPDATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response; + }); + } + + async patchState( + workspaceSlug: string, + projectId: string, + stateId: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`, data) + .then((response) => { + trackEventService.trackStateEvent(response?.data, "STATE_UPDATE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteState(workspaceSlug: string, projectId: string, stateId: string, user: IUser | undefined): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`) + .then((response) => { + trackEventService.trackStateEvent(response?.data, "STATE_DELETE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response; + }); + } +} diff --git a/web/services/track_event.service.ts b/web/services/track_event.service.ts new file mode 100644 index 00000000000..d3a2bd74381 --- /dev/null +++ b/web/services/track_event.service.ts @@ -0,0 +1,850 @@ +// services +import { APIService } from "services/api.service"; + +const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; + +// types +import type { + IUser, + ICycle, + IEstimate, + IGptResponse, + IIssue, + IIssueComment, + IModule, + IPage, + IPageBlock, + IProject, + IState, + IProjectView, + IWorkspace, + IssueCommentReaction, + IssueReaction, +} from "types"; + +type WorkspaceEventType = + | "CREATE_WORKSPACE" + | "UPDATE_WORKSPACE" + | "DELETE_WORKSPACE" + | "WORKSPACE_USER_INVITE" + | "WORKSPACE_USER_INVITE_ACCEPT" + | "WORKSPACE_USER_BULK_INVITE_ACCEPT"; + +type ProjectEventType = + | "CREATE_PROJECT" + | "UPDATE_PROJECT" + | "DELETE_PROJECT" + | "PROJECT_MEMBER_INVITE" + | "PROJECT_MEMBER_LEAVE"; + +type IssueEventType = "ISSUE_CREATE" | "ISSUE_UPDATE" | "ISSUE_DELETE"; + +type CycleEventType = "CYCLE_CREATE" | "CYCLE_UPDATE" | "CYCLE_DELETE"; + +type StateEventType = "STATE_CREATE" | "STATE_UPDATE" | "STATE_DELETE"; + +type ModuleEventType = "MODULE_CREATE" | "MODULE_UPDATE" | "MODULE_DELETE"; + +type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE"; + +type ViewEventType = "VIEW_CREATE" | "VIEW_UPDATE" | "VIEW_DELETE"; + +type IssueCommentEventType = "ISSUE_COMMENT_CREATE" | "ISSUE_COMMENT_UPDATE" | "ISSUE_COMMENT_DELETE"; + +type Toggle = "TOGGLE_CYCLE" | "TOGGLE_MODULE" | "TOGGLE_VIEW" | "TOGGLE_PAGES" | "TOGGLE_STATE" | "TOGGLE_INBOX"; + +export type MiscellaneousEventType = `${Toggle}_ON` | `${Toggle}_OFF`; + +type IntegrationEventType = "ADD_WORKSPACE_INTEGRATION" | "REMOVE_WORKSPACE_INTEGRATION"; + +type GitHubSyncEventType = "GITHUB_REPO_SYNC"; + +type PageBlocksEventType = + | "PAGE_BLOCK_CREATE" + | "PAGE_BLOCK_UPDATE" + | "PAGE_BLOCK_DELETE" + | "PAGE_BLOCK_CONVERTED_TO_ISSUE"; + +type IssueLabelEventType = "ISSUE_LABEL_CREATE" | "ISSUE_LABEL_UPDATE" | "ISSUE_LABEL_DELETE"; + +type GptEventType = "ASK_GPT" | "USE_GPT_RESPONSE_IN_ISSUE" | "USE_GPT_RESPONSE_IN_PAGE_BLOCK"; + +type IssueEstimateEventType = "ESTIMATE_CREATE" | "ESTIMATE_UPDATE" | "ESTIMATE_DELETE"; + +type InboxEventType = + | "INBOX_CREATE" + | "INBOX_UPDATE" + | "INBOX_DELETE" + | "INBOX_ISSUE_CREATE" + | "INBOX_ISSUE_UPDATE" + | "INBOX_ISSUE_DELETE" + | "INBOX_ISSUE_DUPLICATED" + | "INBOX_ISSUE_ACCEPTED" + | "INBOX_ISSUE_SNOOZED" + | "INBOX_ISSUE_REJECTED"; + +type ImporterEventType = + | "GITHUB_IMPORTER_CREATE" + | "GITHUB_IMPORTER_DELETE" + | "JIRA_IMPORTER_CREATE" + | "JIRA_IMPORTER_DELETE"; + +type ExporterEventType = "CSV_EXPORTER_CREATE"; + +type AnalyticsEventType = + | "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" + | "WORKSPACE_CUSTOM_ANALYTICS" + | "WORKSPACE_ANALYTICS_EXPORT" + | "PROJECT_SCOPE_AND_DEMAND_ANALYTICS" + | "PROJECT_CUSTOM_ANALYTICS" + | "PROJECT_ANALYTICS_EXPORT" + | "CYCLE_SCOPE_AND_DEMAND_ANALYTICS" + | "CYCLE_CUSTOM_ANALYTICS" + | "CYCLE_ANALYTICS_EXPORT" + | "MODULE_SCOPE_AND_DEMAND_ANALYTICS" + | "MODULE_CUSTOM_ANALYTICS" + | "MODULE_ANALYTICS_EXPORT"; + +type ReactionEventType = + | "ISSUE_REACTION_CREATE" + | "ISSUE_COMMENT_REACTION_CREATE" + | "ISSUE_REACTION_DELETE" + | "ISSUE_COMMENT_REACTION_DELETE"; + +export class TrackEventService extends APIService { + constructor() { + super("/"); + } + + async trackWorkspaceEvent(data: IWorkspace | any, eventName: WorkspaceEventType, user: IUser): Promise { + if (!trackEvent) return; + + let payload: any; + if ( + eventName !== "DELETE_WORKSPACE" && + eventName !== "WORKSPACE_USER_INVITE" && + eventName !== "WORKSPACE_USER_INVITE_ACCEPT" && + eventName !== "WORKSPACE_USER_BULK_INVITE_ACCEPT" + ) + payload = { + workspaceId: data.id, + workspaceSlug: data.slug, + workspaceName: data.name, + }; + else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackProjectEvent(data: Partial | any, eventName: ProjectEventType, user: IUser): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName !== "DELETE_PROJECT" && eventName !== "PROJECT_MEMBER_INVITE" && eventName !== "PROJECT_MEMBER_LEAVE") + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.id, + projectName: data?.name, + }; + else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackUserOnboardingCompleteEvent(data: any, user: IUser): Promise { + if (!trackEvent) return; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName: "USER_ONBOARDING_COMPLETE", + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackUserTourCompleteEvent(data: any, user: IUser): Promise { + if (!trackEvent) return; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName: "USER_TOUR_COMPLETE", + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackIssueEvent(data: IIssue | any, eventName: IssueEventType, user: IUser): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName !== "ISSUE_DELETE") + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data?.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + issueId: data?.id, + }; + else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackIssueMarkedAsDoneEvent(data: any, user: IUser): Promise { + if (!trackEvent) return; + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName: "ISSUES_MARKED_AS_DONE", + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackIssuePartialPropertyUpdateEvent( + data: any, + propertyName: + | "ISSUE_PROPERTY_UPDATE_PRIORITY" + | "ISSUE_PROPERTY_UPDATE_STATE" + | "ISSUE_PROPERTY_UPDATE_ASSIGNEE" + | "ISSUE_PROPERTY_UPDATE_DUE_DATE" + | "ISSUE_PROPERTY_UPDATE_ESTIMATE", + user: IUser + ): Promise { + if (!trackEvent) return; + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName: propertyName, + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackIssueCommentEvent( + data: Partial | any, + eventName: IssueCommentEventType, + user: IUser | undefined + ): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName !== "ISSUE_COMMENT_DELETE") + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data?.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + issueId: data?.issue, + }; + else payload = data; + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackIssueRelationEvent( + data: any, + eventName: "ISSUE_RELATION_CREATE" | "ISSUE_RELATION_DELETE", + user: IUser + ): Promise { + if (!trackEvent) return; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: data, + user: user, + }, + }); + } + + async trackIssueMovedToCycleOrModuleEvent( + data: any, + eventName: + | "ISSUE_MOVED_TO_CYCLE" + | "ISSUE_MOVED_TO_MODULE" + | "ISSUE_MOVED_TO_CYCLE_IN_BULK" + | "ISSUE_MOVED_TO_MODULE_IN_BULK", + user: IUser + ): Promise { + if (!trackEvent) return; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackIssueBulkDeleteEvent(data: any, user: IUser): Promise { + if (!trackEvent) return; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName: "ISSUE_BULK_DELETE", + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackIssueLabelEvent(data: any, eventName: IssueLabelEventType, user: IUser): Promise { + if (!trackEvent) return; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackStateEvent(data: IState | any, eventName: StateEventType, user: IUser): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName !== "STATE_DELETE") + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data?.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + stateId: data.id, + }; + else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackCycleEvent(data: ICycle | any, eventName: CycleEventType, user: IUser): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName !== "CYCLE_DELETE") + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data?.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + cycleId: data.id, + }; + else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackModuleEvent(data: IModule | any, eventName: ModuleEventType, user: IUser): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName !== "MODULE_DELETE") + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + moduleId: data.id, + }; + else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackPageEvent(data: Partial | any, eventName: PagesEventType, user: IUser | undefined): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName !== "PAGE_DELETE") + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data?.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + pageId: data.id, + }; + else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackPageBlockEvent( + data: Partial | IIssue, + eventName: PageBlocksEventType, + user: IUser + ): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName !== "PAGE_BLOCK_DELETE" && eventName !== "PAGE_BLOCK_CONVERTED_TO_ISSUE") + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data?.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + pageId: (data as IPageBlock)?.page, + pageBlockId: data.id, + }; + else if (eventName === "PAGE_BLOCK_CONVERTED_TO_ISSUE") { + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data?.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + issueId: data?.id, + }; + } else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackAskGptEvent(data: IGptResponse, eventName: GptEventType, user: IUser): Promise { + if (!trackEvent) return; + + const payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectIdentifier: data?.project_detail?.identifier, + projectName: data?.project_detail?.name, + count: data?.count, + }; + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackUseGPTResponseEvent(data: IIssue | IPageBlock, eventName: GptEventType, user: IUser): Promise { + if (!trackEvent) return; + + let payload: any; + + if (eventName === "USE_GPT_RESPONSE_IN_ISSUE") { + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectIdentifier: data?.project_detail?.identifier, + projectName: data?.project_detail?.name, + issueId: data.id, + }; + } else if (eventName === "USE_GPT_RESPONSE_IN_PAGE_BLOCK") { + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectIdentifier: data?.project_detail?.identifier, + projectName: data?.project_detail?.name, + pageId: (data as IPageBlock)?.page, + pageBlockId: data.id, + }; + } + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackViewEvent(data: IProjectView, eventName: ViewEventType, user: IUser): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName === "VIEW_DELETE") payload = data; + else + payload = { + labels: Boolean(data.query_data.labels), + assignees: Boolean(data.query_data.assignees), + priority: Boolean(data.query_data.priority), + state: Boolean(data.query_data.state), + created_by: Boolean(data.query_data.created_by), + }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackMiscellaneousEvent(data: any, eventName: MiscellaneousEventType, user: IUser | undefined): Promise { + if (!trackEvent || !user) return; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackAppIntegrationEvent(data: any, eventName: IntegrationEventType, user: IUser): Promise { + if (!trackEvent) return; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackGitHubSyncEvent(data: any, eventName: GitHubSyncEventType, user: IUser): Promise { + if (!trackEvent) return; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...data, + }, + user: user, + }, + }); + } + + async trackIssueEstimateEvent( + data: { estimate: IEstimate }, + eventName: IssueEstimateEventType, + user: IUser + ): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName === "ESTIMATE_DELETE") payload = data; + else + payload = { + workspaceId: data?.estimate?.workspace_detail?.id, + workspaceName: data?.estimate?.workspace_detail?.name, + workspaceSlug: data?.estimate?.workspace_detail?.slug, + projectId: data?.estimate?.project_detail?.id, + projectName: data?.estimate?.project_detail?.name, + projectIdentifier: data?.estimate?.project_detail?.identifier, + estimateId: data.estimate?.id, + }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackImporterEvent(data: any, eventName: ImporterEventType, user: IUser | undefined): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName === "GITHUB_IMPORTER_DELETE" || eventName === "JIRA_IMPORTER_DELETE") payload = data; + else + payload = { + workspaceId: data?.workspace_detail?.id, + workspaceName: data?.workspace_detail?.name, + workspaceSlug: data?.workspace_detail?.slug, + projectId: data?.project_detail?.id, + projectName: data?.project_detail?.name, + projectIdentifier: data?.project_detail?.identifier, + }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackAnalyticsEvent(data: any, eventName: AnalyticsEventType, user: IUser): Promise { + if (!trackEvent) return; + + const payload = { ...data }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: payload, + user: user, + }, + }); + } + + async trackExporterEvent(data: any, eventName: ExporterEventType, user: IUser): Promise { + if (!trackEvent) return; + + const payload = { ...data }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + // TODO: add types to the data + async trackInboxEvent(data: any, eventName: InboxEventType, user: IUser): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName !== "INBOX_DELETE") + payload = { + issue: data?.issue?.id, + inbox: data?.id, + workspaceId: data?.issue?.workspace_detail?.id, + workspaceName: data?.issue?.workspace_detail?.name, + workspaceSlug: data?.issue?.workspace_detail?.slug, + projectId: data?.issue?.project_detail?.id, + projectName: data?.issue?.project_detail?.name, + }; + else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + user: user, + }, + }); + } + + async trackReactionEvent( + data: IssueReaction | IssueCommentReaction, + eventName: ReactionEventType, + user: IUser + ): Promise { + if (!trackEvent) return; + + let payload: any; + if (eventName === "ISSUE_REACTION_DELETE" || eventName === "ISSUE_COMMENT_REACTION_DELETE") payload = data; + else + payload = { + workspaceId: data?.workspace, + projectId: data?.project, + reaction: data?.reaction, + }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: payload, + user: user, + }, + }); + } + + async trackProjectPublishSettingsEvent(data: any, eventName: string, user: IUser): Promise { + if (!trackEvent) return; + + const payload: any = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: payload, + user: user, + }, + }); + } +} diff --git a/web/services/user.service.ts b/web/services/user.service.ts new file mode 100644 index 00000000000..f5c4ac17e6e --- /dev/null +++ b/web/services/user.service.ts @@ -0,0 +1,192 @@ +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import type { + IIssue, + IUser, + IUserActivityResponse, + IUserProfileData, + IUserProfileProjectSegregation, + IUserSettings, + IUserWorkspaceDashboard, +} from "types"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class UserService extends APIService { + constructor() { + super(API_BASE_URL); + } + + currentUserConfig() { + return { + url: `${this.baseURL}/api/users/me/`, + headers: this.getHeaders(), + }; + } + + async userIssues( + workspaceSlug: string, + params: any + ): Promise< + | { + [key: string]: IIssue[]; + } + | IIssue[] + > { + return this.get(`/api/workspaces/${workspaceSlug}/my-issues/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async currentUser(): Promise { + return this.get("/api/users/me/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async currentUserSettings(): Promise { + return this.get("/api/users/me/settings/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async updateUser(data: Partial): Promise { + return this.patch("/api/users/me/", data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateUserOnBoard({ userRole }: any, user: IUser | undefined): Promise { + return this.patch("/api/users/me/onboard/", { + is_onboarded: true, + }) + .then((response) => { + trackEventService.trackUserOnboardingCompleteEvent( + { + user_role: userRole ?? "None", + }, + user as IUser + ); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateUserTourCompleted(user: IUser): Promise { + return this.patch("/api/users/me/tour-completed/", { + is_tour_completed: true, + }) + .then((response) => { + trackEventService.trackUserTourCompleteEvent({ user_role: user.role ?? "None" }, user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUserWorkspaceActivity(workspaceSlug: string): Promise { + return this.get(`/api/users/workspaces/${workspaceSlug}/activities/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async userWorkspaceDashboard(workspaceSlug: string, month: number): Promise { + return this.get(`/api/users/me/workspaces/${workspaceSlug}/dashboard/`, { + params: { + month: month, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async forgotPassword(data: { email: string }): Promise { + return this.post(`/api/forgot-password/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async resetPassword( + uidb64: string, + token: string, + data: { + new_password: string; + confirm_password: string; + } + ): Promise { + return this.post(`/api/reset-password/${uidb64}/${token}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUserProfileData(workspaceSlug: string, userId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/user-stats/${userId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUserProfileProjectsSegregation( + workspaceSlug: string, + userId: string + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/user-profile/${userId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUserProfileActivity(workspaceSlug: string, userId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/user-activity/${userId}/?per_page=15`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUserProfileIssues( + workspaceSlug: string, + userId: string, + params: any + ): Promise< + | { + [key: string]: IIssue[]; + } + | IIssue[] + > { + return this.get(`/api/workspaces/${workspaceSlug}/user-issues/${userId}/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/view.service.ts b/web/services/view.service.ts new file mode 100644 index 00000000000..97f0db9a2e8 --- /dev/null +++ b/web/services/view.service.ts @@ -0,0 +1,99 @@ +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// types +import { IProjectView } from "types/views"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; + +const trackEventService = new TrackEventService(); + +export class ViewService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async createView(workspaceSlug: string, projectId: string, data: Partial, user: any): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data) + .then((response) => { + trackEventService.trackViewEvent(response?.data, "VIEW_CREATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchView( + workspaceSlug: string, + projectId: string, + viewId: string, + data: Partial, + user: any + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data) + .then((response) => { + trackEventService.trackViewEvent(response?.data, "VIEW_UPDATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteView(workspaceSlug: string, projectId: string, viewId: string, user: any): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`) + .then((response) => { + trackEventService.trackViewEvent(response?.data, "VIEW_DELETE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getViews(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getViewDetails(workspaceSlug: string, projectId: string, viewId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getViewIssues(workspaceSlug: string, projectId: string, viewId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/issues/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async addViewToFavorites( + workspaceSlug: string, + projectId: string, + data: { + view: string; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-views/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async removeViewFromFavorites(workspaceSlug: string, projectId: string, viewId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-favorite-views/${viewId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts new file mode 100644 index 00000000000..30126c2eeac --- /dev/null +++ b/web/services/workspace.service.ts @@ -0,0 +1,287 @@ +// services +import { APIService } from "services/api.service"; +import { TrackEventService } from "services/track_event.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; +// types +import { + IWorkspace, + IWorkspaceMemberMe, + IWorkspaceMember, + IWorkspaceMemberInvitation, + ILastActiveWorkspaceDetails, + IWorkspaceSearchResults, + IProductUpdateResponse, + IUser, + IWorkspaceBulkInviteFormData, + IWorkspaceViewProps, +} from "types"; +import { IWorkspaceView } from "types/workspace-views"; +// store +import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "store/issue"; + +const trackEventService = new TrackEventService(); + +export class WorkspaceService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async userWorkspaces(): Promise { + return this.get("/api/users/me/workspaces/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getWorkspace(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async createWorkspace(data: Partial, user: IUser | undefined): Promise { + return this.post("/api/workspaces/", data) + .then((response) => { + trackEventService.trackWorkspaceEvent(response.data, "CREATE_WORKSPACE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateWorkspace( + workspaceSlug: string, + data: Partial, + user: IUser | undefined + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/`, data) + .then((response) => { + trackEventService.trackWorkspaceEvent(response.data, "UPDATE_WORKSPACE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteWorkspace(workspaceSlug: string, user: IUser | undefined): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/`) + .then((response) => { + trackEventService.trackWorkspaceEvent({ workspaceSlug }, "DELETE_WORKSPACE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async inviteWorkspace( + workspaceSlug: string, + data: IWorkspaceBulkInviteFormData, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/invite/`, data) + .then((response) => { + trackEventService.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async joinWorkspace(workspaceSlug: string, invitationId: string, data: any, user: IUser | undefined): Promise { + return this.post(`/api/users/me/invitations/workspaces/${workspaceSlug}/${invitationId}/join/`, data, { + headers: {}, + }) + .then((response) => { + trackEventService.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE_ACCEPT", user as IUser); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + async joinWorkspaces(data: any): Promise { + return this.post("/api/users/me/invitations/workspaces/", data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getLastActiveWorkspaceAndProjects(): Promise { + return this.get("/api/users/last-visited-workspace/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async userWorkspaceInvitations(): Promise { + return this.get("/api/users/me/invitations/workspaces/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async workspaceMemberMe(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/me/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async updateWorkspaceView(workspaceSlug: string, data: { view_props: IWorkspaceViewProps }): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async fetchWorkspaceMembers(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/members/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateWorkspaceMember( + workspaceSlug: string, + memberId: string, + data: Partial + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/members/${memberId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteWorkspaceMember(workspaceSlug: string, memberId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/members/${memberId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async workspaceInvitations(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/invitations/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getWorkspaceInvitation(invitationId: string): Promise { + return this.get(`/api/users/me/invitations/${invitationId}/`, { headers: {} }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteWorkspaceInvitations(workspaceSlug: string, invitationId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async workspaceSlugCheck(slug: string): Promise { + return this.get(`/api/workspace-slug-check/?slug=${slug}`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async searchWorkspace( + workspaceSlug: string, + params: { + project_id?: string; + search: string; + workspace_search: boolean; + } + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/search/`, { + params, + }) + .then((res) => res?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async getProductUpdates(): Promise { + return this.get("/api/release-notes/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createView(workspaceSlug: string, data: Partial): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/views/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateView(workspaceSlug: string, viewId: string, data: Partial): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteView(workspaceSlug: string, viewId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/views/${viewId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getAllViews(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/views/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getViewDetails(workspaceSlug: string, viewId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/views/${viewId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getViewIssues( + workspaceSlug: string, + params: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/issues/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/store/archived-issues/index.ts b/web/store/archived-issues/index.ts new file mode 100644 index 00000000000..b1230f1c9e4 --- /dev/null +++ b/web/store/archived-issues/index.ts @@ -0,0 +1,3 @@ +export * from "./issue.store"; +export * from "./issue_filters.store"; +export * from "./issue_detail.store"; diff --git a/web/store/archived-issues/issue.store.ts b/web/store/archived-issues/issue.store.ts new file mode 100644 index 00000000000..624f4714a49 --- /dev/null +++ b/web/store/archived-issues/issue.store.ts @@ -0,0 +1,230 @@ +import { observable, action, computed, makeObservable, runInAction, autorun } from "mobx"; +// store +import { RootStore } from "../root"; +// types +import { IIssue } from "types"; +// services +import { IssueArchiveService } from "services/issue"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; +import { + IIssueGroupWithSubGroupsStructure, + IIssueGroupedStructure, + IIssueType, + IIssueUnGroupedStructure, +} from "store/issue"; + +export interface IArchivedIssueStore { + loader: boolean; + error: any | null; + // issues + issues: { + [project_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + issueDetail: { + [project_id: string]: { + [issue_id: string]: IIssue; + }; + }; + + // services + archivedIssueService: IssueArchiveService; + + // computed + getIssueType: IIssueType | null; + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + + // action + fetchIssues: (workspaceSlug: string, projectId: string) => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + deleteArchivedIssue: (group: string | null, sub_group: string | null, issue: IIssue) => Promise; +} + +export class ArchivedIssueStore implements IArchivedIssueStore { + loader: boolean = false; + error: any | null = null; + issues: { + [project_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + } = {}; + issueDetail: IArchivedIssueStore["issueDetail"] = {}; + + // service + archivedIssueService; + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + issues: observable.ref, + + // computed + getIssueType: computed, + getIssues: computed, + + // actions + fetchIssues: action, + updateIssueStructure: action, + deleteArchivedIssue: action, + }); + this.rootStore = _rootStore; + this.archivedIssueService = new IssueArchiveService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + + if ( + workspaceSlug && + projectId && + this.rootStore.archivedIssueFilters.userDisplayFilters && + this.rootStore.archivedIssueFilters.userFilters + ) + this.fetchIssues(workspaceSlug, projectId); + }); + } + + get getIssueType() { + const issueSubGroup = this.rootStore.archivedIssueFilters.userDisplayFilters?.sub_group_by || null; + return issueSubGroup ? "groupWithSubGroups" : "grouped"; + } + + get getIssues() { + const projectId: string | null = this.rootStore?.project?.projectId; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + return this.issues?.[projectId]?.[issueType] || null; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }, + }; + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; + }); + }; + + fetchIssues = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + this.error = null; + + this.rootStore.workspace.setWorkspaceSlug(workspaceSlug); + this.rootStore.project.setProjectId(projectId); + + const params = this.rootStore.archivedIssueFilters.appliedFilters; + const issueResponse = await this.archivedIssueService.getArchivedIssues(workspaceSlug, projectId, params); + + const issueType = this.getIssueType; + if (issueType != null) { + const _issues = { + ...this.issues, + [projectId]: { + ...this.issues[projectId], + [issueType]: issueResponse, + }, + }; + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + this.loader = false; + this.error = error; + throw error; + } + }; + + /** + * @description Function to delete issue from the store. NOTE: This function is not deleting issue from the backend. + */ + deleteArchivedIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues?.[group_id]?.filter((i) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues?.[sub_group_id], + [group_id]: issues?.[sub_group_id]?.[group_id]?.filter((i) => i?.id !== issue?.id), + }, + }; + } + + runInAction(() => { + this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; + }); + }; +} diff --git a/web/store/archived-issues/issue_detail.store.ts b/web/store/archived-issues/issue_detail.store.ts new file mode 100644 index 00000000000..ef3468f1995 --- /dev/null +++ b/web/store/archived-issues/issue_detail.store.ts @@ -0,0 +1,198 @@ +import { observable, action, makeObservable, runInAction, computed } from "mobx"; +// store +import { RootStore } from "../root"; +// types +import { IIssue } from "types"; +// services +import { IssueArchiveService } from "services/issue"; + +export interface IArchivedIssueDetailStore { + loader: boolean; + error: any | null; + // issues + issueDetail: { + [issue_id: string]: IIssue; + }; + peekId: string | null; + + // services + archivedIssueService: IssueArchiveService; + + // computed + getIssue: IIssue | null; + + // action + deleteArchivedIssue: (workspaceSlug: string, projectId: string, issuesId: string) => Promise; + unarchiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + fetchPeekIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise; +} + +export class ArchivedIssueDetailStore implements IArchivedIssueDetailStore { + loader: boolean = false; + error: any | null = null; + issueDetail: IArchivedIssueDetailStore["issueDetail"] = {}; + peekId: string | null = null; + + // service + archivedIssueService; + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + issueDetail: observable.ref, + peekId: observable.ref, + + // computed + getIssue: computed, + + // actions + deleteArchivedIssue: action, + unarchiveIssue: action, + fetchIssueDetails: action, + fetchPeekIssueDetails: action, + }); + this.rootStore = _rootStore; + this.archivedIssueService = new IssueArchiveService(); + } + + get getIssue() { + if (!this.peekId) return null; + const _issue = this.issueDetail[this.peekId]; + return _issue || null; + } + + /** + * @description Function to delete archived issue from the detail store and backend. + */ + deleteArchivedIssue = async (workspaceSlug: string, projectId: string, issuesId: string) => { + const originalIssues = { ...this.issueDetail }; + + const _issues = { ...this.issueDetail }; + delete _issues[issuesId]; + + // optimistically deleting item from store + runInAction(() => { + this.issueDetail = _issues; + }); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + // deleting using api + const issueResponse = await this.archivedIssueService.deleteArchivedIssue(workspaceSlug, projectId, issuesId); + + runInAction(() => { + this.loader = false; + this.error = null; + }); + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + runInAction(() => { + this.loader = false; + this.error = error; + // reverting back to original issues + this.issueDetail = originalIssues; + }); + throw error; + } + }; + + fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const issueResponse = await this.archivedIssueService.retrieveArchivedIssue(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.loader = false; + this.error = null; + this.issueDetail = { + ...this.issueDetail, + [issueId]: issueResponse, + }; + }); + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + runInAction(() => { + this.loader = false; + this.error = error; + }); + throw error; + } + }; + + unarchiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const issueResponse = await this.archivedIssueService.unarchiveIssue(workspaceSlug, projectId, issueId); + this.rootStore.archivedIssues.fetchIssues(workspaceSlug, projectId); + + // deleting from issue detail store + const _issues = { ...this.issueDetail }; + delete _issues[issueId]; + runInAction(() => { + this.issueDetail = _issues; + }); + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + runInAction(() => { + this.loader = false; + this.error = error; + }); + throw error; + } + }; + + fetchPeekIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => { + runInAction(() => { + this.loader = true; + this.error = null; + this.peekId = issueId; + }); + + try { + const issueResponse = await this.archivedIssueService.retrieveArchivedIssue(workspaceSlug, projectId, issueId); + await this.rootStore.issueDetail.fetchIssueReactions(workspaceSlug, projectId, issueId); + await this.rootStore.issueDetail.fetchIssueComments(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.loader = false; + this.error = null; + this.issueDetail = { + ...this.issueDetail, + [issueId]: issueResponse, + }; + }); + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + runInAction(() => { + this.loader = false; + this.error = error; + this.peekId = null; + }); + throw error; + } + }; +} diff --git a/web/store/archived-issues/issue_filters.store.ts b/web/store/archived-issues/issue_filters.store.ts new file mode 100644 index 00000000000..edb1549a630 --- /dev/null +++ b/web/store/archived-issues/issue_filters.store.ts @@ -0,0 +1,244 @@ +import { observable, computed, makeObservable, action, runInAction } from "mobx"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// services +import { IssueService } from "services/issue"; +import { ProjectService } from "services/project"; +// types +import { RootStore } from "../root"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TIssueParams, + IProjectViewProps, +} from "types"; + +export interface IArchivedIssueFilterStore { + loader: boolean; + error: any | null; + + // observables + userDisplayProperties: IIssueDisplayProperties; + userDisplayFilters: IIssueDisplayFilterOptions; + userFilters: IIssueFilterOptions; + + // services + projectService: ProjectService; + issueService: IssueService; + + // computed + appliedFilters: TIssueParams[] | null; + + // actions + fetchUserProjectFilters: (workspaceSlug: string, projectId: string) => Promise; + updateUserFilters: ( + workspaceSlug: string, + projectId: string, + properties: Partial + ) => Promise; + updateDisplayProperties: ( + workspaceSlug: string, + projectId: string, + properties: Partial + ) => Promise; +} + +export class ArchivedIssueFilterStore implements IArchivedIssueFilterStore { + loader: boolean = false; + error: any | null = null; + + // observables + userFilters: IIssueFilterOptions = { + priority: null, + state_group: null, + labels: null, + start_date: null, + target_date: null, + assignees: null, + created_by: null, + subscriber: null, + }; + userDisplayFilters: IIssueDisplayFilterOptions = { + group_by: null, + order_by: "sort_order", + show_empty_groups: true, + type: null, + layout: "list", + }; + userDisplayProperties: any = { + assignee: true, + start_date: true, + due_date: true, + labels: true, + key: true, + priority: true, + state: true, + sub_issue_count: true, + link: true, + attachment_count: true, + estimate: true, + created_on: true, + updated_on: true, + }; + + // root store + rootStore; + + // services + projectService: ProjectService; + issueService: IssueService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + userFilters: observable.ref, + userDisplayFilters: observable.ref, + userDisplayProperties: observable.ref, + + // computed + appliedFilters: computed, + + // actions + fetchUserProjectFilters: action, + updateUserFilters: action, + updateDisplayProperties: action, + computedFilter: action, + }); + + this.rootStore = _rootStore; + + // services + this.issueService = new IssueService(); + this.projectService = new ProjectService(); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + if (!this.userFilters || !this.userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.userFilters?.priority || undefined, + state_group: this.userFilters?.state_group || undefined, + state: this.userFilters?.state || undefined, + assignees: this.userFilters?.assignees || undefined, + created_by: this.userFilters?.created_by || undefined, + labels: this.userFilters?.labels || undefined, + start_date: this.userFilters?.start_date || undefined, + target_date: this.userFilters?.target_date || undefined, + group_by: this.userDisplayFilters?.group_by, + order_by: this.userDisplayFilters?.order_by || "-created_at", + sub_group_by: this.userDisplayFilters?.sub_group_by || undefined, + type: this.userDisplayFilters?.type || undefined, + sub_issue: this.userDisplayFilters?.sub_issue || true, + show_empty_groups: this.userDisplayFilters?.show_empty_groups || true, + start_target_date: this.userDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout("list", "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + return filteredRouteParams; + } + + updateUserFilters = async (workspaceSlug: string, projectId: string, properties: Partial) => { + const newViewProps = { + display_filters: { + ...this.userDisplayFilters, + ...properties.display_filters, + }, + filters: { + ...this.userFilters, + ...properties.filters, + }, + }; + + // set sub_group_by to null if group_by is set to null + if (newViewProps.display_filters.group_by === null) newViewProps.display_filters.sub_group_by = null; + + // set group_by to state if layout is switched to kanban and group_by is null + if (newViewProps.display_filters.layout === "kanban" && newViewProps.display_filters.group_by === null) + newViewProps.display_filters.group_by = "state"; + + try { + runInAction(() => { + this.userFilters = newViewProps.filters; + this.userDisplayFilters = { + ...newViewProps.display_filters, + layout: "list", + }; + }); + + this.projectService.setProjectView(workspaceSlug, projectId, { + view_props: newViewProps, + }); + } catch (error) { + this.fetchUserProjectFilters(workspaceSlug, projectId); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user filters in issue filter store", error); + } + }; + + updateDisplayProperties = async ( + workspaceSlug: string, + projectId: string, + properties: Partial + ) => { + const newProperties: IIssueDisplayProperties = { + ...this.userDisplayProperties, + ...properties, + }; + + try { + runInAction(() => { + this.userDisplayProperties = newProperties; + }); + + await this.issueService.updateIssueDisplayProperties(workspaceSlug, projectId, newProperties); + } catch (error) { + this.fetchUserProjectFilters(workspaceSlug, projectId); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user display properties in issue filter store", error); + } + }; + + fetchUserProjectFilters = async (workspaceSlug: string, projectId: string) => { + try { + const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId); + const issueProperties = await this.issueService.getIssueDisplayProperties(workspaceSlug, projectId); + + runInAction(() => { + this.userFilters = memberResponse?.view_props?.filters; + this.userDisplayFilters = { + ...(memberResponse?.view_props?.display_filters ?? {}), + layout: "list", + }; + this.userDisplayProperties = issueProperties?.properties; + }); + } catch (error) { + runInAction(() => { + this.error = error; + }); + + console.log("Failed to fetch user filters in issue filter store", error); + } + }; +} diff --git a/web/store/calendar.store.ts b/web/store/calendar.store.ts new file mode 100644 index 00000000000..3d4f5cfdde0 --- /dev/null +++ b/web/store/calendar.store.ts @@ -0,0 +1,121 @@ +import { observable, action, makeObservable, runInAction, computed } from "mobx"; + +// helpers +import { generateCalendarData } from "helpers/calendar.helper"; +// types +import { RootStore } from "./root"; +import { ICalendarPayload, ICalendarWeek } from "components/issues"; +import { getWeekNumberOfDate } from "helpers/date-time.helper"; + +export interface ICalendarStore { + calendarFilters: { + activeMonthDate: Date; + activeWeekDate: Date; + }; + calendarPayload: ICalendarPayload | null; + + // action + updateCalendarFilters: (filters: Partial<{ activeMonthDate: Date; activeWeekDate: Date }>) => void; + updateCalendarPayload: (date: Date) => void; + + // computed + allWeeksOfActiveMonth: + | { + [weekNumber: string]: ICalendarWeek; + } + | undefined; + activeWeekNumber: number; + allDaysOfActiveWeek: ICalendarWeek | undefined; +} + +class CalendarStore implements ICalendarStore { + loader: boolean = false; + error: any | null = null; + + // observables + calendarFilters: { activeMonthDate: Date; activeWeekDate: Date } = { + activeMonthDate: new Date(), + activeWeekDate: new Date(), + }; + calendarPayload: ICalendarPayload | null = null; + + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + loader: observable.ref, + error: observable.ref, + + // observables + calendarFilters: observable.ref, + calendarPayload: observable.ref, + + // actions + updateCalendarFilters: action, + updateCalendarPayload: action, + + //computed + allWeeksOfActiveMonth: computed, + activeWeekNumber: computed, + allDaysOfActiveWeek: computed, + }); + + this.rootStore = _rootStore; + + this.initCalendar(); + } + + get allWeeksOfActiveMonth() { + if (!this.calendarPayload) return undefined; + + const { activeMonthDate } = this.calendarFilters; + + return this.calendarPayload[`y-${activeMonthDate.getFullYear()}`][`m-${activeMonthDate.getMonth()}`]; + } + + get activeWeekNumber() { + return getWeekNumberOfDate(this.calendarFilters.activeWeekDate); + } + + get allDaysOfActiveWeek() { + if (!this.calendarPayload) return undefined; + + const { activeWeekDate } = this.calendarFilters; + + return this.calendarPayload[`y-${activeWeekDate.getFullYear()}`][`m-${activeWeekDate.getMonth()}`][ + `w-${this.activeWeekNumber}` + ]; + } + + updateCalendarFilters = (filters: Partial<{ activeMonthDate: Date; activeWeekDate: Date }>) => { + this.updateCalendarPayload(filters.activeMonthDate || filters.activeWeekDate || new Date()); + + runInAction(() => { + this.calendarFilters = { + ...this.calendarFilters, + ...filters, + }; + }); + }; + + updateCalendarPayload = (date: Date) => { + if (!this.calendarPayload) return null; + + const nextDate = new Date(date); + + runInAction(() => { + this.calendarPayload = generateCalendarData(this.calendarPayload, nextDate); + }); + }; + + initCalendar = () => { + const newCalendarPayload = generateCalendarData(null, new Date()); + + runInAction(() => { + this.calendarPayload = newCalendarPayload; + }); + }; +} + +export default CalendarStore; diff --git a/web/store/command-palette.store.ts b/web/store/command-palette.store.ts new file mode 100644 index 00000000000..7bed73bfebc --- /dev/null +++ b/web/store/command-palette.store.ts @@ -0,0 +1,163 @@ +import { observable, action, makeObservable } from "mobx"; +// types +import { RootStore } from "./root"; +// services +import { ProjectService } from "services/project"; +import { PageService } from "services/page.service"; + +export interface ICommandPaletteStore { + isCommandPaletteOpen: boolean; + isShortcutModalOpen: boolean; + isCreateProjectModalOpen: boolean; + isCreateCycleModalOpen: boolean; + isCreateModuleModalOpen: boolean; + isCreateViewModalOpen: boolean; + isCreatePageModalOpen: boolean; + isCreateIssueModalOpen: boolean; + isDeleteIssueModalOpen: boolean; + isBulkDeleteIssueModalOpen: boolean; + + toggleCommandPaletteModal: (value?: boolean) => void; + toggleShortcutModal: (value?: boolean) => void; + toggleCreateProjectModal: (value?: boolean) => void; + toggleCreateCycleModal: (value?: boolean) => void; + toggleCreateViewModal: (value?: boolean) => void; + toggleCreatePageModal: (value?: boolean) => void; + toggleCreateIssueModal: (value?: boolean) => void; + toggleCreateModuleModal: (value?: boolean) => void; + toggleDeleteIssueModal: (value?: boolean) => void; + toggleBulkDeleteIssueModal: (value?: boolean) => void; +} + +class CommandPaletteStore implements ICommandPaletteStore { + isCommandPaletteOpen: boolean = false; + isShortcutModalOpen: boolean = false; + isCreateProjectModalOpen: boolean = false; + isCreateCycleModalOpen: boolean = false; + isCreateModuleModalOpen: boolean = false; + isCreateViewModalOpen: boolean = false; + isCreatePageModalOpen: boolean = false; + isCreateIssueModalOpen: boolean = false; + isDeleteIssueModalOpen: boolean = false; + isBulkDeleteIssueModalOpen: boolean = false; + // root store + rootStore; + // service + projectService; + pageService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + isCommandPaletteOpen: observable.ref, + isShortcutModalOpen: observable.ref, + isCreateProjectModalOpen: observable.ref, + isCreateCycleModalOpen: observable.ref, + isCreateModuleModalOpen: observable.ref, + isCreateViewModalOpen: observable.ref, + isCreatePageModalOpen: observable.ref, + isCreateIssueModalOpen: observable.ref, + isDeleteIssueModalOpen: observable.ref, + isBulkDeleteIssueModalOpen: observable.ref, + // computed + // projectPages: computed, + // action + toggleCommandPaletteModal: action, + toggleShortcutModal: action, + toggleCreateProjectModal: action, + toggleCreateCycleModal: action, + toggleCreateViewModal: action, + toggleCreatePageModal: action, + toggleCreateIssueModal: action, + toggleCreateModuleModal: action, + toggleDeleteIssueModal: action, + toggleBulkDeleteIssueModal: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.pageService = new PageService(); + } + + toggleCommandPaletteModal = (value?: boolean) => { + if (value) { + this.isCommandPaletteOpen = value; + } else { + this.isCommandPaletteOpen = !this.isCommandPaletteOpen; + } + }; + + toggleShortcutModal = (value?: boolean) => { + if (value) { + this.isShortcutModalOpen = value; + } else { + this.isShortcutModalOpen = !this.isShortcutModalOpen; + } + }; + + toggleCreateProjectModal = (value?: boolean) => { + if (value) { + this.isCreateProjectModalOpen = value; + } else { + this.isCreateProjectModalOpen = !this.isCreateProjectModalOpen; + } + }; + + toggleCreateCycleModal = (value?: boolean) => { + if (value) { + this.isCreateCycleModalOpen = value; + } else { + this.isCreateCycleModalOpen = !this.isCreateCycleModalOpen; + } + }; + + toggleCreateViewModal = (value?: boolean) => { + if (value) { + this.isCreateViewModalOpen = value; + } else { + this.isCreateViewModalOpen = !this.isCreateViewModalOpen; + } + }; + + toggleCreatePageModal = (value?: boolean) => { + if (value) { + this.isCreatePageModalOpen = value; + } else { + this.isCreatePageModalOpen = !this.isCreatePageModalOpen; + } + }; + + toggleCreateIssueModal = (value?: boolean) => { + if (value) { + this.isCreateIssueModalOpen = value; + } else { + this.isCreateIssueModalOpen = !this.isCreateIssueModalOpen; + } + }; + + toggleDeleteIssueModal = (value?: boolean) => { + if (value) { + this.isDeleteIssueModalOpen = value; + } else { + this.isDeleteIssueModalOpen = !this.isDeleteIssueModalOpen; + } + }; + + toggleCreateModuleModal = (value?: boolean) => { + if (value) { + this.isCreateModuleModalOpen = value; + } else { + this.isCreateModuleModalOpen = !this.isCreateModuleModalOpen; + } + }; + + toggleBulkDeleteIssueModal = (value?: boolean) => { + if (value) { + this.isBulkDeleteIssueModalOpen = value; + } else { + this.isBulkDeleteIssueModalOpen = !this.isBulkDeleteIssueModalOpen; + } + }; +} + +export default CommandPaletteStore; diff --git a/web/store/cycle/cycle_issue.store.ts b/web/store/cycle/cycle_issue.store.ts new file mode 100644 index 00000000000..17d8f351cbf --- /dev/null +++ b/web/store/cycle/cycle_issue.store.ts @@ -0,0 +1,364 @@ +import { observable, action, computed, makeObservable, runInAction, autorun } from "mobx"; +// store +import { RootStore } from "../root"; +// services +import { CycleService } from "services/cycle.service"; +import { IssueService } from "services/issue"; +// constants +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; +// types +import { IIssue } from "types"; +import { IBlockUpdateData } from "components/gantt-chart"; +import { + IIssueGroupWithSubGroupsStructure, + IIssueGroupedStructure, + IIssueType, + IIssueUnGroupedStructure, +} from "store/issue"; + +export interface ICycleIssueStore { + loader: boolean; + error: any | null; + // issues + issues: { + [cycleId: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + // computed + getIssueType: IIssueType | null; + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + getIssuesCount: number; + // action + fetchIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + 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, issueIds: string[]) => Promise; + removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) => void; +} + +export class CycleIssueStore implements ICycleIssueStore { + loader: boolean = false; + error: any | null = null; + issues: { + [cycle_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + } = {}; + + // services + rootStore; + cycleService; + issueService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + issues: observable.ref, + // computed + getIssueType: computed, + getIssues: computed, + getIssuesCount: computed, + // actions + fetchIssues: action, + updateIssueStructure: action, + updateGanttIssueStructure: action, + deleteIssue: action, + addIssueToCycle: action, + removeIssueFromCycle: action, + }); + + this.rootStore = _rootStore; + this.cycleService = new CycleService(); + this.issueService = new IssueService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + const cycleId = this.rootStore.cycle.cycleId; + + if ( + workspaceSlug && + projectId && + cycleId && + this.rootStore.cycleIssueFilter.cycleFilters && + this.rootStore.issueFilter.userDisplayFilters + ) + this.fetchIssues(workspaceSlug, projectId, cycleId); + }); + } + + get getIssueType() { + const groupedLayouts = ["kanban", "list", "calendar"]; + const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; + + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null; + + if (!issueLayout) return null; + + const _issueState = groupedLayouts.includes(issueLayout) + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : ungroupedLayouts.includes(issueLayout) + ? "ungrouped" + : null; + + return _issueState || null; + } + + get getIssues() { + const cycleId: string | null = this.rootStore?.cycle?.cycleId; + const issueType = this.getIssueType; + if (!cycleId || !issueType) return null; + + return this.issues?.[cycleId]?.[issueType] || null; + } + + get getIssuesCount() { + const issueType = this.getIssueType; + + let issuesCount = 0; + + if (issueType === "grouped") { + const issues = this.getIssues as IIssueGroupedStructure; + + if (!issues) return 0; + + Object.keys(issues).map((group_id) => { + issuesCount += issues[group_id].length; + }); + } + + if (issueType === "groupWithSubGroups") { + const issues = this.getIssues as IIssueGroupWithSubGroupsStructure; + + if (!issues) return 0; + + Object.keys(issues).map((sub_group_id) => { + Object.keys(issues[sub_group_id]).map((group_id) => { + issuesCount += issues[sub_group_id][group_id].length; + }); + }); + } + + if (issueType === "ungrouped") { + const issues = this.getIssues as IIssueUnGroupedStructure; + + if (!issues) return 0; + + issuesCount = issues.length; + } + + return issuesCount; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const cycleId: string | null = this.rootStore?.cycle?.cycleId || null; + const issueType = this.getIssueType; + if (!cycleId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)); + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") issues = sortArrayByDate(issues as any, "created_at"); + if (orderBy === "-updated_at") issues = sortArrayByDate(issues as any, "updated_at"); + if (orderBy === "start_date") issues = sortArrayByDate(issues as any, "updated_at"); + if (orderBy === "priority") issues = sortArrayByPriority(issues as any, "priority"); + + runInAction(() => { + this.issues = { ...this.issues, [cycleId]: { ...this.issues[cycleId], [issueType]: issues } }; + }); + }; + + updateGanttIssueStructure = async ( + workspaceSlug: string, + cycleId: string, + issue: IIssue, + payload: IBlockUpdateData + ) => { + if (!issue || !workspaceSlug) return; + + const issues = this.getIssues as IIssueUnGroupedStructure; + + const newIssues = issues.map((i) => ({ + ...i, + ...(i.id === issue.id + ? { + sort_order: payload.sort_order?.newSortOrder ?? i.sort_order, + start_date: payload.start_date, + target_date: payload.target_date, + } + : {}), + })); + + if (payload.sort_order) { + const removedElement = newIssues.splice(payload.sort_order.sourceIndex, 1)[0]; + removedElement.sort_order = payload.sort_order.newSortOrder; + newIssues.splice(payload.sort_order.destinationIndex, 0, removedElement); + } + + runInAction(() => { + this.issues = { + ...this.issues, + [cycleId]: { + ...this.issues[cycleId], + ungrouped: newIssues, + }, + }; + }); + + const newPayload: any = { ...payload }; + + if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; + + this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload); + }; + + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const cycleId: string | null = this.rootStore.cycle.cycleId; + const issueType = this.getIssueType; + if (!cycleId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i) => i?.id !== issue?.id); + } + + runInAction(() => { + this.issues = { ...this.issues, [cycleId]: { ...this.issues[cycleId], [issueType]: issues } }; + }); + }; + + fetchIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + this.loader = true; + this.error = null; + + const params = this.rootStore?.cycleIssueFilter?.appliedFilters; + const issueResponse = await this.cycleService.getCycleIssuesWithParams(workspaceSlug, projectId, cycleId, params); + + const issueType = this.getIssueType; + if (issueType != null) { + const _issues = { + ...this.issues, + [cycleId]: { + ...this.issues[cycleId], + [issueType]: issueResponse, + }, + }; + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + this.loader = false; + this.error = error; + return error; + } + }; + + addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { + try { + const user = this.rootStore.user.currentUser ?? undefined; + + await this.issueService.addIssueToCycle( + workspaceSlug, + projectId, + cycleId, + { + issues: issueIds, + }, + user + ); + + this.fetchIssues(workspaceSlug, projectId, cycleId); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, bridgeId: string) => { + try { + await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, bridgeId); + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, cycleId); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; +} diff --git a/web/store/cycle/cycle_issue_calendar_view.store.ts b/web/store/cycle/cycle_issue_calendar_view.store.ts new file mode 100644 index 00000000000..3b695d69778 --- /dev/null +++ b/web/store/cycle/cycle_issue_calendar_view.store.ts @@ -0,0 +1,89 @@ +import { action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueType } from "store/issue"; + +export interface ICycleIssueCalendarViewStore { + // actions + handleDragDrop: (source: any, destination: any) => void; +} + +export class CycleIssueCalendarViewStore implements ICycleIssueCalendarViewStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // actions + handleDragDrop: action, + }); + + this.rootStore = _rootStore; + } + + handleDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const cycleId = this.rootStore?.cycle?.cycleId; + const issueType: IIssueType | null = this.rootStore?.cycleIssue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.cycleIssue.getIssues; + + if (workspaceSlug && projectId && cycleId && issueType && issueLayout === "calendar" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; + + if (droppableSourceColumnId === droppableDestinationColumnId) return; + + if (droppableSourceColumnId != droppableDestinationColumnId) { + // horizontal + const _sourceIssues = currentIssues[droppableSourceColumnId]; + let _destinationIssues = currentIssues[droppableDestinationColumnId] || []; + + const [removed] = _sourceIssues.splice(source.index, 1); + + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + target_date: droppableDestinationColumnId, + }); + else _destinationIssues = [..._destinationIssues, { ...removed, target_date: droppableDestinationColumnId }]; + + updateIssue = { ...updateIssue, issueId: removed?.id, target_date: droppableDestinationColumnId }; + + currentIssues[droppableSourceColumnId] = _sourceIssues; + currentIssues[droppableDestinationColumnId] = _destinationIssues; + } + + const reorderedIssues = { + ...this.rootStore?.cycleIssue.issues, + [cycleId]: { + ...this.rootStore?.cycleIssue.issues?.[cycleId], + [issueType]: { + ...this.rootStore?.cycleIssue.issues?.[cycleId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.cycleIssue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + + return; + }; +} diff --git a/web/store/cycle/cycle_issue_filters.store.ts b/web/store/cycle/cycle_issue_filters.store.ts new file mode 100644 index 00000000000..c6818b6df21 --- /dev/null +++ b/web/store/cycle/cycle_issue_filters.store.ts @@ -0,0 +1,147 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// services +import { CycleService } from "services/cycle.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssueFilterOptions, TIssueParams } from "types"; + +export interface ICycleIssueFilterStore { + loader: boolean; + error: any | null; + cycleFilters: IIssueFilterOptions; + + // action + fetchCycleFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + updateCycleFilters: ( + workspaceSlug: string, + projectId: string, + cycleId: string, + filterToUpdate: Partial + ) => Promise; + + // computed + appliedFilters: TIssueParams[] | null; +} + +export class CycleIssueFilterStore implements ICycleIssueFilterStore { + // observables + loader: boolean = false; + error: any | null = null; + cycleFilters: IIssueFilterOptions = {}; + // root store + rootStore; + // services + cycleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + loader: observable.ref, + error: observable.ref, + cycleFilters: observable.ref, + // computed + appliedFilters: computed, + // actions + fetchCycleFilters: action, + updateCycleFilters: action, + }); + + this.rootStore = _rootStore; + this.cycleService = new CycleService(); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + const userDisplayFilters = this.rootStore?.issueFilter?.userDisplayFilters; + + if (!this.cycleFilters || !userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.cycleFilters?.priority || undefined, + state_group: this.cycleFilters?.state_group || undefined, + state: this.cycleFilters?.state || undefined, + assignees: this.cycleFilters?.assignees || undefined, + created_by: this.cycleFilters?.created_by || undefined, + labels: this.cycleFilters?.labels || undefined, + start_date: this.cycleFilters?.start_date || undefined, + target_date: this.cycleFilters?.target_date || undefined, + group_by: userDisplayFilters?.group_by || "state", + order_by: userDisplayFilters?.order_by || "-created_at", + sub_group_by: userDisplayFilters?.sub_group_by || undefined, + type: userDisplayFilters?.type || undefined, + sub_issue: userDisplayFilters?.sub_issue || true, + show_empty_groups: userDisplayFilters?.show_empty_groups || true, + start_target_date: userDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(userDisplayFilters.layout, "issues"); + + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + fetchCycleFilters = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const cycleResponse = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId); + runInAction(() => { + this.cycleFilters = cycleResponse?.view_props?.filters || {}; + }); + } catch (error) { + runInAction(() => { + this.error = error; + }); + + console.log("Failed to fetch user filters in issue filter store", error); + } + }; + + updateCycleFilters = async ( + workspaceSlug: string, + projectId: string, + cycleId: string, + properties: Partial + ) => { + const newProperties = { + ...this.cycleFilters, + ...properties, + }; + + try { + runInAction(() => { + this.cycleFilters = newProperties; + }); + + const payload = { + view_props: { + filters: newProperties, + }, + }; + + await this.cycleService.updateCycle(workspaceSlug, projectId, cycleId, payload, undefined); + } catch (error) { + this.fetchCycleFilters(workspaceSlug, projectId, cycleId); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user filters in issue filter store", error); + } + }; +} diff --git a/web/store/cycle/cycle_issue_kanban_view.store.ts b/web/store/cycle/cycle_issue_kanban_view.store.ts new file mode 100644 index 00000000000..0ecc96e60ad --- /dev/null +++ b/web/store/cycle/cycle_issue_kanban_view.store.ts @@ -0,0 +1,448 @@ +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueType } from "store/issue"; + +export interface ICycleIssueKanBanViewStore { + kanBanToggle: { + groupByHeaderMinMax: string[]; + subgroupByIssuesVisibility: string[]; + }; + // computed + canUserDragDrop: boolean; + canUserDragDropVertically: boolean; + canUserDragDropHorizontally: boolean; + // actions + handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void; + handleSwimlaneDragDrop: (source: any, destination: any) => void; + handleDragDrop: (source: any, destination: any) => void; +} + +export class CycleIssueKanBanViewStore implements ICycleIssueKanBanViewStore { + kanBanToggle: { + groupByHeaderMinMax: string[]; + subgroupByIssuesVisibility: string[]; + } = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] }; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + kanBanToggle: observable, + // computed + canUserDragDrop: computed, + canUserDragDropVertically: computed, + canUserDragDropHorizontally: computed, + + // actions + handleKanBanToggle: action, + handleSwimlaneDragDrop: action, + handleDragDrop: action, + }); + + this.rootStore = _rootStore; + } + + get canUserDragDrop() { + if (this.rootStore.issueDetail.peekId) return false; + if ( + this.rootStore?.issueFilter?.userDisplayFilters?.order_by && + this.rootStore?.issueFilter?.userDisplayFilters?.order_by === "sort_order" && + this.rootStore?.issueFilter?.userDisplayFilters?.group_by && + ["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.group_by) + ) { + if (!this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by) return true; + if ( + this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by && + ["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by) + ) + return true; + } + return false; + } + + get canUserDragDropVertically() { + return false; + } + + get canUserDragDropHorizontally() { + return false; + } + + handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { + this.kanBanToggle = { + ...this.kanBanToggle, + [toggle]: this.kanBanToggle[toggle].includes(value) + ? this.kanBanToggle[toggle].filter((v) => v !== value) + : [...this.kanBanToggle[toggle], value], + }; + }; + + handleSwimlaneDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.cycleIssue.getIssues; + + const sortOrderDefaultValue = 65535; + + if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + // source, destination group and sub group id + let droppableSourceColumnId = source?.droppableId || null; + droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; + let droppableDestinationColumnId = destination?.droppableId || null; + droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; + if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; + + const source_group_id: string = droppableSourceColumnId[0]; + const source_sub_group_id: string = droppableSourceColumnId[1] === "null" ? null : droppableSourceColumnId[1]; + + const destination_group_id: string = droppableDestinationColumnId[0]; + const destination_sub_group_id: string = + droppableDestinationColumnId[1] === "null" ? null : droppableDestinationColumnId[1]; + + if (source_sub_group_id === destination_sub_group_id) { + if (source_group_id === destination_group_id) { + const _issues = currentIssues[source_sub_group_id][source_group_id]; + + // update the sort order + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _issues.length - 1) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2, + }; + } + + const [removed] = _issues.splice(source.index, 1); + _issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order }); + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_sub_group_id][source_group_id] = _issues; + } + + if (source_group_id != destination_group_id) { + const _sourceIssues = currentIssues[source_sub_group_id][source_group_id]; + let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + let issueStatePriority = {}; + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") { + updateIssue = { ...updateIssue, state: destination_group_id }; + issueStatePriority = { ...issueStatePriority, state: destination_group_id }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") { + updateIssue = { ...updateIssue, priority: destination_group_id }; + issueStatePriority = { ...issueStatePriority, priority: destination_group_id }; + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + ...issueStatePriority, + }); + else + _destinationIssues = [ + ..._destinationIssues, + { ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority }, + ]; + updateIssue = { ...updateIssue, issueId: removed?.id }; + + currentIssues[source_sub_group_id][source_group_id] = _sourceIssues; + currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues; + } + } + + if (source_sub_group_id != destination_sub_group_id) { + const _sourceIssues = currentIssues[source_sub_group_id][source_group_id]; + let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + let issueStatePriority = {}; + if (source_group_id === destination_group_id) { + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "state") { + updateIssue = { ...updateIssue, state: destination_sub_group_id }; + issueStatePriority = { ...issueStatePriority, state: destination_sub_group_id }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "priority") { + updateIssue = { ...updateIssue, priority: destination_sub_group_id }; + issueStatePriority = { ...issueStatePriority, priority: destination_sub_group_id }; + } + } else { + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "state") { + updateIssue = { ...updateIssue, state: destination_sub_group_id, priority: destination_group_id }; + issueStatePriority = { + ...issueStatePriority, + state: destination_sub_group_id, + priority: destination_group_id, + }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "priority") { + updateIssue = { ...updateIssue, state: destination_group_id, priority: destination_sub_group_id }; + issueStatePriority = { + ...issueStatePriority, + state: destination_group_id, + priority: destination_sub_group_id, + }; + } + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + ...issueStatePriority, + }); + else + _destinationIssues = [ + ..._destinationIssues, + { ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority }, + ]; + + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_sub_group_id][source_group_id] = _sourceIssues; + currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues; + } + + const reorderedIssues = { + ...this.rootStore?.cycleIssue.issues, + [projectId]: { + ...this.rootStore?.cycleIssue.issues?.[projectId], + [issueType]: { + ...this.rootStore?.cycleIssue.issues?.[projectId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.cycleIssue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + }; + + handleDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.cycleIssue.getIssues; + + const sortOrderDefaultValue = 65535; + + if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + // source, destination group and sub group id + let droppableSourceColumnId = source?.droppableId || null; + droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; + let droppableDestinationColumnId = destination?.droppableId || null; + droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; + if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; + + const source_group_id: string = droppableSourceColumnId[0]; + const destination_group_id: string = droppableDestinationColumnId[0]; + + if (this.canUserDragDrop) { + // vertical + if (source_group_id === destination_group_id) { + const _issues = currentIssues[source_group_id]; + + // update the sort order + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _issues.length - 1) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2, + }; + } + + const [removed] = _issues.splice(source.index, 1); + _issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order }); + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_group_id] = _issues; + } + + // horizontal + if (source_group_id != destination_group_id) { + const _sourceIssues = currentIssues[source_group_id]; + let _destinationIssues = currentIssues[destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + let issueStatePriority = {}; + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") { + updateIssue = { ...updateIssue, state: destination_group_id }; + issueStatePriority = { ...issueStatePriority, state: destination_group_id }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") { + updateIssue = { ...updateIssue, priority: destination_group_id }; + issueStatePriority = { ...issueStatePriority, priority: destination_group_id }; + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + ...issueStatePriority, + }); + else + _destinationIssues = [ + ..._destinationIssues, + { ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority }, + ]; + updateIssue = { ...updateIssue, issueId: removed?.id }; + + currentIssues[source_group_id] = _sourceIssues; + currentIssues[destination_group_id] = _destinationIssues; + } + } + + // user can drag the issues only vertically + if (this.canUserDragDropVertically && destination_group_id === destination_group_id) { + } + + // user can drag the issues only horizontally + if (this.canUserDragDropHorizontally && destination_group_id != destination_group_id) { + } + + const reorderedIssues = { + ...this.rootStore?.cycleIssue.issues, + [projectId]: { + ...this.rootStore?.cycleIssue.issues?.[projectId], + [issueType]: { + ...this.rootStore?.cycleIssue.issues?.[projectId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.cycleIssue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + }; +} diff --git a/web/store/cycle/cycles.store.ts b/web/store/cycle/cycles.store.ts new file mode 100644 index 00000000000..06ac6185465 --- /dev/null +++ b/web/store/cycle/cycles.store.ts @@ -0,0 +1,358 @@ +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +// types +import { ICycle, TCycleView, TCycleLayout, CycleDateCheckData, IIssue } from "types"; +// mobx +import { RootStore } from "../root"; +// services +import { ProjectService } from "services/project"; +import { IssueService } from "services/issue"; +import { CycleService } from "services/cycle.service"; + +export interface ICycleStore { + loader: boolean; + error: any | null; + + cycleView: TCycleView; + cycleLayout: TCycleLayout; + + cycleId: string | null; + cycles: { + [project_id: string]: ICycle[]; + }; + cycle_details: { + [cycle_id: string]: ICycle; + }; + active_cycle_issues: { + [cycle_id: string]: IIssue[]; + }; + + // computed + getCycleById: (cycleId: string) => ICycle | null; + + // actions + setCycleView: (_cycleView: TCycleView) => void; + setCycleLayout: (_cycleLayout: TCycleLayout) => void; + setCycleId: (cycleId: string) => void; + + validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; + + fetchCycles: ( + workspaceSlug: string, + projectId: string, + params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" + ) => Promise; + 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; + removeCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + + addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; +} + +export class CycleStore implements ICycleStore { + loader: boolean = false; + error: any | null = null; + + cycleView: TCycleView = "all"; + cycleLayout: TCycleLayout = "list"; + + cycleId: string | null = null; + cycles: { + [project_id: string]: ICycle[]; + } = {}; + + cycle_details: { + [cycle_id: string]: ICycle; + } = {}; + + active_cycle_issues: { + [cycle_id: string]: IIssue[]; + } = {}; + + // root store + rootStore; + // services + projectService; + issueService; + cycleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + loader: observable, + error: observable.ref, + + cycleView: observable, + cycleLayout: observable, + + cycleId: observable, + cycles: observable.ref, + cycle_details: observable.ref, + active_cycle_issues: observable.ref, + + // computed + projectCycles: computed, + + // actions + setCycleView: action, + setCycleLayout: action, + setCycleId: action, + getCycleById: action, + + fetchCycles: action, + fetchCycleWithId: action, + + fetchActiveCycleIssues: action, + + createCycle: action, + updateCycle: action, + removeCycle: action, + + addCycleToFavorites: action, + removeCycleFromFavorites: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.issueService = new IssueService(); + this.cycleService = new CycleService(); + } + + // computed + get projectCycles() { + if (!this.rootStore.project.projectId) return null; + return this.cycles[this.rootStore.project.projectId] || null; + } + + getCycleById = (cycleId: string) => this.cycle_details[cycleId] || null; + + // actions + setCycleView = (_cycleView: TCycleView) => (this.cycleView = _cycleView); + setCycleLayout = (_cycleLayout: TCycleLayout) => (this.cycleLayout = _cycleLayout); + setCycleId = (cycleId: string) => (this.cycleId = cycleId); + + validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => { + try { + const response = await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload); + return response; + } catch (error) { + console.log("Failed to validate cycle dates", error); + throw error; + } + }; + + fetchCycles = async ( + workspaceSlug: string, + projectId: string, + params: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" + ) => { + try { + this.loader = true; + this.error = null; + + const cyclesResponse = await this.cycleService.getCyclesWithParams(workspaceSlug, projectId, params); + + if (this.cycleView === "active") this.fetchActiveCycleIssues(workspaceSlug, projectId, cyclesResponse[0].id); + + runInAction(() => { + this.cycles = { + ...this.cycles, + [projectId]: cyclesResponse, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + console.error("Failed to fetch project cycles in project store", error); + this.loader = false; + this.error = error; + } + }; + + fetchCycleWithId = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const response = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId); + + runInAction(() => { + this.cycle_details = { + ...this.cycle_details, + [response?.id]: response, + }; + }); + return response; + } catch (error) { + console.log("Failed to fetch cycle detail from cycle store"); + throw error; + } + }; + + fetchActiveCycleIssues = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const _cycleIssues = await this.cycleService.getCycleIssuesWithParams(workspaceSlug, projectId, cycleId, { + priority: `urgent,high`, + }); + + const _activeCycleIssues = { + ...this.active_cycle_issues, + [cycleId]: _cycleIssues as IIssue[], + }; + + runInAction(() => { + this.active_cycle_issues = _activeCycleIssues; + }); + + return _activeCycleIssues; + } catch (error) { + console.log("error"); + } + }; + + createCycle = async (workspaceSlug: string, projectId: string, data: any) => { + 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 = { + ...this.cycle_details, + [response?.id]: response, + }; + }); + + const _currentView = this.cycleView === "active" ? "current" : this.cycleView; + this.fetchCycles(workspaceSlug, projectId, _currentView); + + return response; + } catch (error) { + console.log("Failed to create cycle from cycle store"); + throw error; + } + }; + + 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) => { + try { + const _response = await this.cycleService.patchCycle(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 patch cycle from cycle store"); + throw error; + } + }; + + removeCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const _response = await this.cycleService.deleteCycle(workspaceSlug, projectId, cycleId, undefined); + + const _currentView = this.cycleView === "active" ? "current" : this.cycleView; + this.fetchCycles(workspaceSlug, projectId, _currentView); + + return _response; + } catch (error) { + console.log("Failed to delete cycle from cycle store"); + throw error; + } + }; + + addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + runInAction(() => { + this.cycles = { + ...this.cycles, + [projectId]: this.cycles[projectId].map((cycle) => { + if (cycle.id === cycleId) return { ...cycle, is_favorite: true }; + return cycle; + }), + }; + }); + // updating through api. + const response = await this.cycleService.addCycleToFavorites(workspaceSlug, projectId, { cycle: cycleId }); + return response; + } catch (error) { + console.log("Failed to add cycle to favorites in the cycles store", error); + // resetting the local state + runInAction(() => { + this.cycles = { + ...this.cycles, + [projectId]: this.cycles[projectId].map((cycle) => { + if (cycle.id === cycleId) return { ...cycle, is_favorite: false }; + return cycle; + }), + }; + }); + + throw error; + } + }; + + removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + runInAction(() => { + this.cycles = { + ...this.cycles, + [projectId]: this.cycles[projectId].map((cycle) => { + if (cycle.id === cycleId) return { ...cycle, is_favorite: false }; + return cycle; + }), + }; + }); + const response = await this.cycleService.removeCycleFromFavorites(workspaceSlug, projectId, cycleId); + return response; + } catch (error) { + console.log("Failed to remove cycle from favorites - Cycle Store", error); + runInAction(() => { + this.cycles = { + ...this.cycles, + [projectId]: this.cycles[projectId].map((cycle) => { + if (cycle.id === cycleId) return { ...cycle, is_favorite: true }; + return cycle; + }), + }; + }); + throw error; + } + }; +} diff --git a/web/store/cycle/index.ts b/web/store/cycle/index.ts new file mode 100644 index 00000000000..ae854f97890 --- /dev/null +++ b/web/store/cycle/index.ts @@ -0,0 +1,5 @@ +export * from "./cycle_issue_filters.store"; +export * from "./cycle_issue_kanban_view.store"; +export * from "./cycle_issue_calendar_view.store"; +export * from "./cycle_issue.store"; +export * from "./cycles.store"; diff --git a/web/store/draft-issues/index.ts b/web/store/draft-issues/index.ts new file mode 100644 index 00000000000..abfd52768ec --- /dev/null +++ b/web/store/draft-issues/index.ts @@ -0,0 +1,2 @@ +export * from "./issue.store"; +export * from "./issue_filters.store"; diff --git a/web/store/draft-issues/issue.store.ts b/web/store/draft-issues/issue.store.ts new file mode 100644 index 00000000000..f31a0bbb451 --- /dev/null +++ b/web/store/draft-issues/issue.store.ts @@ -0,0 +1,184 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// store +import { RootStore } from "../root"; +// types +import { IIssue } from "types"; +// services +import { IssueService } from "services/issue"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; +import { + IIssueGroupWithSubGroupsStructure, + IIssueGroupedStructure, + IIssueType, + IIssueUnGroupedStructure, +} from "store/issue"; + +export interface IDraftIssueStore { + loader: boolean; + error: any | null; + // issues + issues: { + [project_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + // computed + getIssueType: IIssueType | null; + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + // action + fetchIssues: (workspaceSlug: string, projectId: string) => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; +} + +export class DraftIssueStore implements IDraftIssueStore { + loader: boolean = false; + error: any | null = null; + issues: { + [project_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + } = {}; + // service + issueService; + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + issues: observable.ref, + // computed + getIssueType: computed, + getIssues: computed, + // actions + fetchIssues: action, + updateIssueStructure: action, + }); + this.rootStore = _rootStore; + this.issueService = new IssueService(); + } + + get getIssueType() { + const groupedLayouts = ["kanban", "list", "calendar"]; + const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; + + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null; + if (!issueLayout) return null; + + const _issueState = groupedLayouts.includes(issueLayout) + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : ungroupedLayouts.includes(issueLayout) + ? "ungrouped" + : null; + + return _issueState || null; + } + + get getIssues() { + const projectId: string | null = this.rootStore?.project?.projectId; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + return this.issues?.[projectId]?.[issueType] || null; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)); + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; + }); + }; + + fetchIssues = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + this.error = null; + + this.rootStore.workspace.setWorkspaceSlug(workspaceSlug); + this.rootStore.project.setProjectId(projectId); + + const params = this.rootStore?.issueFilter?.appliedFilters; + const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params); + + const issueType = this.getIssueType; + if (issueType != null) { + const _issues = { + ...this.issues, + [projectId]: { + ...this.issues[projectId], + [issueType]: issueResponse, + }, + }; + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + this.loader = false; + this.error = error; + return error; + } + }; +} diff --git a/web/store/draft-issues/issue_filters.store.ts b/web/store/draft-issues/issue_filters.store.ts new file mode 100644 index 00000000000..2560c011bbc --- /dev/null +++ b/web/store/draft-issues/issue_filters.store.ts @@ -0,0 +1,109 @@ +import { observable, computed, makeObservable } from "mobx"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; + +export interface IDraftIssueFilterStore { + userDisplayProperties: IIssueDisplayProperties; + userDisplayFilters: IIssueDisplayFilterOptions; + userFilters: IIssueFilterOptions; + + // computed + appliedFilters: TIssueParams[] | null; +} + +export class DraftIssueFilterStore implements IDraftIssueFilterStore { + // observables + userFilters: IIssueFilterOptions = { + priority: null, + state_group: null, + labels: null, + start_date: null, + target_date: null, + assignees: null, + created_by: null, + subscriber: null, + }; + userDisplayFilters: IIssueDisplayFilterOptions = { + group_by: null, + order_by: "sort_order", + show_empty_groups: true, + type: null, + layout: "list", + }; + userDisplayProperties: any = { + assignee: true, + start_date: true, + due_date: true, + labels: true, + key: true, + priority: true, + state: true, + sub_issue_count: true, + link: true, + attachment_count: true, + estimate: true, + created_on: true, + updated_on: true, + }; + + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + userFilters: observable.ref, + userDisplayFilters: observable.ref, + userDisplayProperties: observable.ref, + + // computed + appliedFilters: computed, + }); + + this.rootStore = _rootStore; + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + if (!this.userFilters || !this.userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.userFilters?.priority || undefined, + state_group: this.userFilters?.state_group || undefined, + state: this.userFilters?.state || undefined, + assignees: this.userFilters?.assignees || undefined, + created_by: this.userFilters?.created_by || undefined, + labels: this.userFilters?.labels || undefined, + start_date: this.userFilters?.start_date || undefined, + target_date: this.userFilters?.target_date || undefined, + group_by: this.userDisplayFilters?.group_by || "state", + order_by: this.userDisplayFilters?.order_by || "-created_at", + sub_group_by: this.userDisplayFilters?.sub_group_by || undefined, + type: this.userDisplayFilters?.type || undefined, + sub_issue: this.userDisplayFilters?.sub_issue || true, + show_empty_groups: this.userDisplayFilters?.show_empty_groups || true, + start_target_date: this.userDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (this.userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (this.userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } +} diff --git a/web/store/editor/index.ts b/web/store/editor/index.ts new file mode 100644 index 00000000000..ff3ce7a3349 --- /dev/null +++ b/web/store/editor/index.ts @@ -0,0 +1 @@ +export * from "./mentions.store" \ No newline at end of file diff --git a/web/store/editor/mentions.store.ts b/web/store/editor/mentions.store.ts new file mode 100644 index 00000000000..4bf1f45c3af --- /dev/null +++ b/web/store/editor/mentions.store.ts @@ -0,0 +1,45 @@ +import { IMentionHighlight, IMentionSuggestion } 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.projectMembers + + 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] : [] + } +} \ No newline at end of file diff --git a/web/store/global-view/global_view_filters.store.ts b/web/store/global-view/global_view_filters.store.ts new file mode 100644 index 00000000000..9bec4bde8a1 --- /dev/null +++ b/web/store/global-view/global_view_filters.store.ts @@ -0,0 +1,68 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueFilterOptions } from "types"; + +export interface IGlobalViewFiltersStore { + // states + loader: boolean; + error: any | null; + + // observables + storedFilters: { + [viewId: string]: IIssueFilterOptions; + }; + + // actions + updateStoredFilters: (viewId: string, filters: Partial) => void; + deleteStoredFilters: (viewId: string) => void; +} + +export class GlobalViewFiltersStore implements IGlobalViewFiltersStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + storedFilters: { + [viewId: string]: IIssueFilterOptions; + } = {}; + + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + storedFilters: observable.ref, + + // actions + updateStoredFilters: action, + deleteStoredFilters: action, + }); + + this.rootStore = _rootStore; + } + + updateStoredFilters = (viewId: string, filters: Partial) => { + runInAction(() => { + this.storedFilters = { + ...this.storedFilters, + [viewId]: { ...this.storedFilters[viewId], ...filters }, + }; + }); + }; + + deleteStoredFilters = (viewId: string) => { + const updatedStoredFilters = { ...this.storedFilters }; + delete updatedStoredFilters[viewId]; + + runInAction(() => { + this.storedFilters = updatedStoredFilters; + }); + }; +} diff --git a/web/store/global-view/global_view_issues.store.ts b/web/store/global-view/global_view_issues.store.ts new file mode 100644 index 00000000000..006c9b380a3 --- /dev/null +++ b/web/store/global-view/global_view_issues.store.ts @@ -0,0 +1,204 @@ +import { observable, action, makeObservable, runInAction, autorun } from "mobx"; +// services +import { ProjectService } from "services/project"; +import { WorkspaceService } from "services/workspace.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssue, IIssueFilterOptions, TStaticViewTypes } from "types"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; + +export interface IGlobalViewIssuesStore { + // states + loader: boolean; + error: any | null; + + // observables + viewIssues: { + [viewId: string]: IIssue[]; + }; + + // actions + fetchViewIssues: (workspaceSlug: string, viewId: string, filters: IIssueFilterOptions) => Promise; + fetchStaticIssues: (workspaceSlug: string, type: TStaticViewTypes) => Promise; + updateIssueStructure: (viewId: string, issue: IIssue) => Promise; +} + +export class GlobalViewIssuesStore implements IGlobalViewIssuesStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + viewIssues: { + [viewId: string]: IIssue[]; + } = {}; + + // root store + rootStore; + + // services + projectService; + workspaceService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + viewIssues: observable.ref, + + // actions + fetchViewIssues: action, + fetchStaticIssues: action, + updateIssueStructure: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.workspaceService = new WorkspaceService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const globalViewId = this.rootStore.globalViews.globalViewId; + + if ( + workspaceSlug && + globalViewId && + this.rootStore.globalViewFilters.storedFilters[globalViewId] && + this.rootStore.issueFilter.userDisplayFilters + ) + this.fetchViewIssues(workspaceSlug, globalViewId, this.rootStore.globalViewFilters.storedFilters[globalViewId]); + }); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + fetchViewIssues = async (workspaceSlug: string, viewId: string, filters: IIssueFilterOptions) => { + try { + runInAction(() => { + this.loader = true; + }); + + const displayFilters = this.rootStore.workspaceFilter.workspaceDisplayFilters; + + let filteredRouteParams: any = { + priority: filters?.priority || undefined, + project: filters?.project || undefined, + state_group: filters?.state_group || undefined, + state: filters?.state || undefined, + assignees: filters?.assignees || undefined, + created_by: filters?.created_by || undefined, + labels: filters?.labels || undefined, + start_date: filters?.start_date || undefined, + target_date: filters?.target_date || undefined, + order_by: displayFilters?.order_by || "-created_at", + type: displayFilters?.type || undefined, + sub_issue: false, + }; + + const filteredParams = handleIssueQueryParamsByLayout("spreadsheet", "my_issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + const response = await this.workspaceService.getViewIssues(workspaceSlug, filteredRouteParams); + + runInAction(() => { + this.loader = false; + this.viewIssues = { + ...this.viewIssues, + [viewId]: response as IIssue[], + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + fetchStaticIssues = async (workspaceSlug: string, type: TStaticViewTypes) => { + try { + runInAction(() => { + this.loader = true; + }); + + const workspaceMemberResponse = await this.rootStore.workspaceFilter.fetchUserWorkspaceFilters(workspaceSlug); + const displayFilters = workspaceMemberResponse.view_props.display_filters; + + let filteredRouteParams: any = { + order_by: displayFilters?.order_by || "-created_at", + type: displayFilters?.type || undefined, + sub_issue: false, + }; + + const filteredParams = handleIssueQueryParamsByLayout("spreadsheet", "my_issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + const currentUser = this.rootStore.user.currentUser; + + if (type === "assigned" && currentUser) filteredRouteParams.assignees = currentUser.id; + if (type === "created" && currentUser) filteredRouteParams.created_by = currentUser.id; + if (type === "subscribed" && currentUser) filteredRouteParams.subscriber = currentUser.id; + + const response = await this.workspaceService.getViewIssues(workspaceSlug, filteredRouteParams); + + runInAction(() => { + this.loader = false; + this.viewIssues = { + ...this.viewIssues, + [type]: response as IIssue[], + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + updateIssueStructure = async (viewId: string, issue: IIssue) => { + let issues = this.viewIssues[viewId]; + + if (!issues) return null; + + const _currentIssueId = issues?.find((_i) => _i?.id === issue.id); + issues = _currentIssueId + ? issues?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) + : [...(issues ?? []), issue]; + + const orderBy = this.rootStore?.workspaceFilter?.workspaceDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") issues = sortArrayByDate(issues as any, "created_at"); + + if (orderBy === "-updated_at") issues = sortArrayByDate(issues as any, "updated_at"); + + if (orderBy === "start_date") issues = sortArrayByDate(issues as any, "updated_at"); + + if (orderBy === "priority") issues = sortArrayByPriority(issues as any, "priority"); + + runInAction(() => { + this.viewIssues = { ...this.viewIssues, [viewId]: issues }; + }); + }; +} diff --git a/web/store/global-view/global_views.store.ts b/web/store/global-view/global_views.store.ts new file mode 100644 index 00000000000..c9915b8d898 --- /dev/null +++ b/web/store/global-view/global_views.store.ts @@ -0,0 +1,205 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// services +import { ProjectService } from "services/project"; +import { WorkspaceService } from "services/workspace.service"; +// types +import { RootStore } from "../root"; +import { IWorkspaceView } from "types/workspace-views"; + +export interface IGlobalViewsStore { + // states + loader: boolean; + error: any | null; + + // observables + globalViewId: string | null; + globalViewsList: IWorkspaceView[] | null; + globalViewDetails: { + [viewId: string]: IWorkspaceView; + }; + + // actions + setGlobalViewId: (viewId: string) => void; + + fetchAllGlobalViews: (workspaceSlug: string) => Promise; + fetchGlobalViewDetails: (workspaceSlug: string, viewId: string) => Promise; + createGlobalView: (workspaceSlug: string, data: Partial) => Promise; + updateGlobalView: (workspaceSlug: string, viewId: string, data: Partial) => Promise; + deleteGlobalView: (workspaceSlug: string, viewId: string) => Promise; +} + +export class GlobalViewsStore implements IGlobalViewsStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + globalViewId: string | null = null; + globalViewsList: IWorkspaceView[] | null = null; + globalViewDetails: { [viewId: string]: IWorkspaceView } = {}; + + // root store + rootStore; + + // services + projectService; + workspaceService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + globalViewId: observable.ref, + globalViewsList: observable.ref, + globalViewDetails: observable.ref, + + // actions + setGlobalViewId: action, + + fetchAllGlobalViews: action, + fetchGlobalViewDetails: action, + createGlobalView: action, + updateGlobalView: action, + deleteGlobalView: action, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.workspaceService = new WorkspaceService(); + } + + setGlobalViewId = (viewId: string) => { + this.globalViewId = viewId; + }; + + fetchAllGlobalViews = async (workspaceSlug: string): Promise => { + try { + runInAction(() => { + this.loader = true; + }); + + const response = await this.workspaceService.getAllViews(workspaceSlug); + + runInAction(() => { + this.loader = false; + this.globalViewsList = response; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + fetchGlobalViewDetails = async (workspaceSlug: string, viewId: string): Promise => { + try { + runInAction(() => { + this.loader = true; + }); + + const response = await this.workspaceService.getViewDetails(workspaceSlug, viewId); + + runInAction(() => { + this.loader = false; + this.globalViewDetails = { + ...this.globalViewDetails, + [response.id]: response, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + createGlobalView = async (workspaceSlug: string, data: Partial): Promise => { + try { + const response = await this.workspaceService.createView(workspaceSlug, data); + + runInAction(() => { + this.globalViewsList = [response, ...(this.globalViewsList ?? [])]; + this.globalViewDetails = { + ...this.globalViewDetails, + [response.id]: response, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + updateGlobalView = async ( + workspaceSlug: string, + viewId: string, + data: Partial + ): Promise => { + const viewToUpdate = { ...this.globalViewDetails[viewId], ...data }; + + try { + runInAction(() => { + this.globalViewsList = (this.globalViewsList ?? []).map((view) => { + if (view.id === viewId) return viewToUpdate; + + return view; + }); + this.globalViewDetails = { + ...this.globalViewDetails, + [viewId]: viewToUpdate, + }; + }); + + const response = await this.workspaceService.updateView(workspaceSlug, viewId, data); + + return response; + } catch (error) { + this.fetchGlobalViewDetails(workspaceSlug, viewId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + deleteGlobalView = async (workspaceSlug: string, viewId: string): Promise => { + const newViewsList = (this.globalViewsList ?? []).filter((view) => view.id !== viewId); + + try { + runInAction(() => { + this.globalViewsList = newViewsList; + }); + + await this.workspaceService.deleteView(workspaceSlug, viewId); + } catch (error) { + this.fetchAllGlobalViews(workspaceSlug); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; +} diff --git a/web/store/global-view/index.ts b/web/store/global-view/index.ts new file mode 100644 index 00000000000..6c62135db41 --- /dev/null +++ b/web/store/global-view/index.ts @@ -0,0 +1,3 @@ +export * from "./global_view_filters.store"; +export * from "./global_view_issues.store"; +export * from "./global_views.store"; diff --git a/web/store/inbox/inbox.store.ts b/web/store/inbox/inbox.store.ts new file mode 100644 index 00000000000..c1ca086098e --- /dev/null +++ b/web/store/inbox/inbox.store.ts @@ -0,0 +1,162 @@ +import { observable, action, makeObservable, runInAction, computed } from "mobx"; +// types +import { RootStore } from "../root"; +// services +import { InboxService } from "services/inbox.service"; +// types +import { IInbox } from "types"; + +export interface IInboxStore { + // states + loader: boolean; + error: any | null; + + // observables + inboxId: string | null; + + inboxesList: { + [projectId: string]: IInbox[]; + }; + inboxDetails: { + [inboxId: string]: IInbox; + }; + + // actions + setInboxId: (inboxId: string) => void; + + getInboxId: (projectId: string) => string | null; + + fetchInboxesList: (workspaceSlug: string, projectId: string) => Promise; + fetchInboxDetails: (workspaceSlug: string, projectId: string, inboxId: string) => Promise; + + // computed + isInboxEnabled: boolean; +} + +export class InboxStore implements IInboxStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + inboxId: string | null = null; + + inboxesList: { + [projectId: string]: IInbox[]; + } = {}; + inboxDetails: { + [inboxId: string]: IInbox; + } = {}; + + // root store + rootStore; + + // services + inboxService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + inboxId: observable.ref, + + inboxesList: observable.ref, + inboxDetails: observable.ref, + + // actions + setInboxId: action, + + fetchInboxesList: action, + getInboxId: action, + + // computed + isInboxEnabled: computed, + }); + + this.rootStore = _rootStore; + this.inboxService = new InboxService(); + } + + get isInboxEnabled() { + const projectId = this.rootStore.project.projectId; + + if (!projectId) return false; + + const projectDetails = this.rootStore.project.project_details[projectId]; + + if (!projectDetails) return false; + + return projectDetails.inbox_view; + } + + getInboxId = (projectId: string) => { + const projectDetails = this.rootStore.project.project_details[projectId]; + + if (!projectDetails || !projectDetails.inbox_view) return null; + + return this.inboxesList[projectId]?.[0]?.id ?? null; + }; + + setInboxId = (inboxId: string) => { + runInAction(() => { + this.inboxId = inboxId; + }); + }; + + fetchInboxesList = async (workspaceSlug: string, projectId: string) => { + try { + runInAction(() => { + this.loader = true; + }); + + const inboxesResponse = await this.inboxService.getInboxes(workspaceSlug, projectId); + + runInAction(() => { + this.loader = false; + this.inboxesList = { + ...this.inboxesList, + [projectId]: inboxesResponse, + }; + }); + + return inboxesResponse; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + fetchInboxDetails = async (workspaceSlug: string, projectId: string, inboxId: string) => { + try { + runInAction(() => { + this.loader = true; + }); + + const inboxDetailsResponse = await this.inboxService.getInboxById(workspaceSlug, projectId, inboxId); + + runInAction(() => { + this.loader = false; + this.inboxDetails = { + ...this.inboxDetails, + [inboxId]: inboxDetailsResponse, + }; + }); + + return inboxDetailsResponse; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; +} diff --git a/web/store/inbox/inbox_filters.store.ts b/web/store/inbox/inbox_filters.store.ts new file mode 100644 index 00000000000..8a7c7ff379b --- /dev/null +++ b/web/store/inbox/inbox_filters.store.ts @@ -0,0 +1,146 @@ +import { observable, action, makeObservable, runInAction, computed } from "mobx"; +// types +import { RootStore } from "../root"; +// services +import { InboxService } from "services/inbox.service"; +// types +import { IInbox, IInboxFilterOptions, IInboxQueryParams } from "types"; + +export interface IInboxFiltersStore { + // states + loader: boolean; + error: any | null; + + // observables + inboxFilters: { + [inboxId: string]: { filters: IInboxFilterOptions }; + }; + + // actions + fetchInboxFilters: (workspaceSlug: string, projectId: string, inboxId: string) => Promise; + updateInboxFilters: ( + workspaceSlug: string, + projectId: string, + inboxId: string, + filters: Partial + ) => Promise; + + // computed + appliedFilters: IInboxQueryParams | null; +} + +export class InboxFiltersStore implements IInboxFiltersStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + inboxFilters: { + [inboxId: string]: { filters: IInboxFilterOptions }; + } = {}; + + // root store + rootStore; + + // services + inboxService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + inboxFilters: observable.ref, + + // actions + fetchInboxFilters: action, + updateInboxFilters: action, + + // computed + appliedFilters: computed, + }); + + this.rootStore = _rootStore; + this.inboxService = new InboxService(); + } + + get appliedFilters(): IInboxQueryParams | null { + const inboxId = this.rootStore.inbox.inboxId; + + if (!inboxId) return null; + + const filtersList = this.inboxFilters[inboxId]?.filters; + + if (!filtersList) return null; + + const filteredRouteParams: IInboxQueryParams = { + priority: filtersList.priority ? filtersList.priority.join(",") : null, + inbox_status: filtersList.inbox_status ? filtersList.inbox_status.join(",") : null, + }; + + return filteredRouteParams; + } + + fetchInboxFilters = async (workspaceSlug: string, projectId: string, inboxId: string) => { + try { + runInAction(() => { + this.loader = true; + }); + + const issuesResponse = await this.inboxService.getInboxById(workspaceSlug, projectId, inboxId); + + runInAction(() => { + this.loader = false; + this.inboxFilters = { + ...this.inboxFilters, + [inboxId]: issuesResponse.view_props, + }; + }); + + return issuesResponse; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + updateInboxFilters = async ( + workspaceSlug: string, + projectId: string, + inboxId: string, + filters: Partial + ) => { + const newViewProps = { + ...this.inboxFilters[inboxId], + filters: { + ...this.inboxFilters[inboxId]?.filters, + ...filters, + }, + }; + + try { + runInAction(() => { + this.inboxFilters = { + ...this.inboxFilters, + [inboxId]: newViewProps, + }; + }); + + await this.inboxService.patchInbox(workspaceSlug, projectId, inboxId, { view_props: newViewProps }); + } catch (error) { + runInAction(() => { + this.error = error; + }); + + this.fetchInboxFilters(workspaceSlug, projectId, inboxId); + + throw error; + } + }; +} diff --git a/web/store/inbox/inbox_issue_detail.store.ts b/web/store/inbox/inbox_issue_detail.store.ts new file mode 100644 index 00000000000..9085264f528 --- /dev/null +++ b/web/store/inbox/inbox_issue_detail.store.ts @@ -0,0 +1,280 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +// services +import { InboxService } from "services/inbox.service"; +// types +import { IInboxIssue, IIssue, TInboxStatus } from "types"; +// constants +import { INBOX_ISSUE_SOURCE } from "constants/inbox"; + +export interface IInboxIssueDetailsStore { + // states + loader: boolean; + error: any | null; + + // observables + issueDetails: { + [issueId: string]: IInboxIssue; + }; + + // actions + fetchIssueDetails: ( + workspaceSlug: string, + projectId: string, + inboxId: string, + issueId: string + ) => Promise; + createIssue: ( + workspaceSlug: string, + projectId: string, + inboxId: string, + data: Partial + ) => Promise; + updateIssue: ( + workspaceSlug: string, + projectId: string, + inboxId: string, + issueId: string, + data: Partial + ) => Promise; + updateIssueStatus: ( + workspaceSlug: string, + projectId: string, + inboxId: string, + issueId: string, + data: TInboxStatus + ) => Promise; + deleteIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise; +} + +export class InboxIssueDetailsStore implements IInboxIssueDetailsStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + issueDetails: { [issueId: string]: IInboxIssue } = {}; + + // root store + rootStore; + + // services + inboxService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + issueDetails: observable.ref, + + // actions + fetchIssueDetails: action, + createIssue: action, + updateIssueStatus: action, + deleteIssue: action, + }); + + this.rootStore = _rootStore; + this.inboxService = new InboxService(); + } + + fetchIssueDetails = async (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => { + try { + runInAction(() => { + this.loader = true; + }); + + const issueResponse = await this.inboxService.getInboxIssueById(workspaceSlug, projectId, inboxId, issueId); + + runInAction(() => { + this.loader = false; + this.issueDetails = { + ...this.issueDetails, + [issueId]: issueResponse, + }; + }); + + return issueResponse; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + createIssue = async (workspaceSlug: string, projectId: string, inboxId: string, data: Partial) => { + const payload = { + issue: { + name: data.name, + description: data.description, + description_html: data.description_html, + priority: data.priority, + }, + source: INBOX_ISSUE_SOURCE, + }; + + try { + const response = await this.inboxService.createInboxIssue( + workspaceSlug, + projectId, + inboxId, + payload, + this.rootStore.user.currentUser ?? undefined + ); + + runInAction(() => { + this.issueDetails = { + ...this.issueDetails, + [response.id]: response, + }; + this.rootStore.inboxIssues.inboxIssues = { + ...this.rootStore.inboxIssues.inboxIssues, + [inboxId]: [response, ...this.rootStore.inboxIssues.inboxIssues[inboxId]], + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + updateIssue = async ( + workspaceSlug: string, + projectId: string, + inboxId: string, + issueId: string, + data: Partial + ) => { + const updatedIssue = { ...this.issueDetails[issueId], ...data }; + + try { + runInAction(() => { + this.issueDetails = { + ...this.issueDetails, + [issueId]: updatedIssue, + }; + this.rootStore.inboxIssues.inboxIssues = { + ...this.rootStore.inboxIssues.inboxIssues, + [inboxId]: this.rootStore.inboxIssues.inboxIssues[inboxId].map((issue) => { + if (issue.issue_inbox[0].id === issueId) return updatedIssue; + + return issue; + }), + }; + }); + + await this.inboxService.patchInboxIssue( + workspaceSlug, + projectId, + inboxId, + issueId, + { issue: data }, + this.rootStore.user.currentUser ?? undefined + ); + } catch (error) { + runInAction(() => { + this.error = error; + }); + + this.rootStore.inboxIssues.fetchInboxIssues(workspaceSlug, projectId, inboxId); + this.fetchIssueDetails(workspaceSlug, projectId, inboxId, issueId); + + throw error; + } + }; + + updateIssueStatus = async ( + workspaceSlug: string, + projectId: string, + inboxId: string, + issueId: string, + data: TInboxStatus + ) => { + const updatedIssue = { ...this.issueDetails[issueId] }; + updatedIssue.issue_inbox[0] = { + ...updatedIssue.issue_inbox[0], + ...data, + }; + + try { + runInAction(() => { + this.issueDetails = { + ...this.issueDetails, + [issueId]: updatedIssue, + }; + this.rootStore.inboxIssues.inboxIssues = { + ...this.rootStore.inboxIssues.inboxIssues, + [inboxId]: this.rootStore.inboxIssues.inboxIssues[inboxId].map((issue) => { + if (issue.issue_inbox[0].id === issueId) return updatedIssue as IInboxIssue; + + return issue; + }), + }; + }); + + await this.inboxService.markInboxStatus( + workspaceSlug, + projectId, + inboxId, + issueId, + data, + this.rootStore.user.currentUser ?? undefined + ); + } catch (error) { + runInAction(() => { + this.error = error; + }); + + this.rootStore.inboxIssues.fetchInboxIssues(workspaceSlug, projectId, inboxId); + this.fetchIssueDetails(workspaceSlug, projectId, inboxId, issueId); + + throw error; + } + }; + + deleteIssue = async (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => { + const updatedIssues = { ...this.issueDetails }; + delete updatedIssues[issueId]; + + try { + runInAction(() => { + this.issueDetails = updatedIssues; + this.rootStore.inboxIssues.inboxIssues = { + ...this.rootStore.inboxIssues.inboxIssues, + [inboxId]: this.rootStore.inboxIssues.inboxIssues[inboxId]?.filter( + (issue) => issue.issue_inbox[0].id !== issueId + ), + }; + }); + + await this.inboxService.deleteInboxIssue( + workspaceSlug, + projectId, + inboxId, + issueId, + this.rootStore.user.currentUser ?? undefined + ); + } catch (error) { + runInAction(() => { + this.error = error; + }); + + this.rootStore.inboxIssues.fetchInboxIssues(workspaceSlug, projectId, inboxId); + this.fetchIssueDetails(workspaceSlug, projectId, inboxId, issueId); + + throw error; + } + }; +} diff --git a/web/store/inbox/inbox_issues.store.ts b/web/store/inbox/inbox_issues.store.ts new file mode 100644 index 00000000000..06c7997b2df --- /dev/null +++ b/web/store/inbox/inbox_issues.store.ts @@ -0,0 +1,93 @@ +import { observable, action, makeObservable, runInAction, autorun } from "mobx"; +// types +import { RootStore } from "../root"; +// services +import { InboxService } from "services/inbox.service"; +// types +import { IInboxIssue } from "types"; + +export interface IInboxIssuesStore { + // states + loader: boolean; + error: any | null; + + // observables + inboxIssues: { + [inboxId: string]: IInboxIssue[]; + }; + + // actions + fetchInboxIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise; +} + +export class InboxIssuesStore implements IInboxIssuesStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + inboxIssues: { + [inboxId: string]: IInboxIssue[]; + } = {}; + + // root store + rootStore; + + // services + inboxService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + inboxIssues: observable.ref, + + // actions + fetchInboxIssues: action, + }); + + this.rootStore = _rootStore; + this.inboxService = new InboxService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + const inboxId = this.rootStore.inbox.inboxId; + + if (workspaceSlug && projectId && inboxId && this.rootStore.inboxFilters.inboxFilters[inboxId]) + this.fetchInboxIssues(workspaceSlug, projectId, inboxId); + }); + } + + fetchInboxIssues = async (workspaceSlug: string, projectId: string, inboxId: string) => { + try { + runInAction(() => { + this.loader = true; + }); + + const queryParams = this.rootStore.inboxFilters.appliedFilters ?? undefined; + + const issuesResponse = await this.inboxService.getInboxIssues(workspaceSlug, projectId, inboxId, queryParams); + + runInAction(() => { + this.loader = false; + this.inboxIssues = { + ...this.inboxIssues, + [inboxId]: issuesResponse, + }; + }); + + return issuesResponse; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; +} diff --git a/web/store/inbox/index.ts b/web/store/inbox/index.ts new file mode 100644 index 00000000000..5ed4153323e --- /dev/null +++ b/web/store/inbox/index.ts @@ -0,0 +1,4 @@ +export * from "./inbox_filters.store"; +export * from "./inbox_issue_detail.store"; +export * from "./inbox_issues.store"; +export * from "./inbox.store"; diff --git a/web/store/issue/index.ts b/web/store/issue/index.ts new file mode 100644 index 00000000000..c66d617e108 --- /dev/null +++ b/web/store/issue/index.ts @@ -0,0 +1,7 @@ +export * from "./issue_detail.store"; +export * from "./issue_draft.store"; +export * from "./issue_filters.store"; +export * from "./issue_kanban_view.store"; +export * from "./issue_calendar_view.store"; +export * from "./issue.store"; +export * from "./issue_quick_add.store"; diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts new file mode 100644 index 00000000000..b2db11fb956 --- /dev/null +++ b/web/store/issue/issue.store.ts @@ -0,0 +1,370 @@ +import { observable, action, computed, makeObservable, runInAction, autorun } from "mobx"; +// store +import { RootStore } from "../root"; +// types +import { IIssue } from "types"; +// services +import { IssueService } from "services/issue"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; +import { IBlockUpdateData } from "components/gantt-chart"; + +export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; +export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; +export type IIssueGroupWithSubGroupsStructure = { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; +}; +export type IIssueUnGroupedStructure = IIssue[]; + +export interface IIssueStore { + loader: boolean; + error: any | null; + // issues + issues: { + [project_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + // computed + getIssueType: IIssueType | null; + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + getIssuesCount: number; + // action + fetchIssues: (workspaceSlug: string, projectId: string) => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + removeIssueFromStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + updateGanttIssueStructure: (workspaceSlug: string, issue: IIssue, payload: IBlockUpdateData) => void; +} + +export class IssueStore implements IIssueStore { + loader: boolean = false; + error: any | null = null; + issues: { + [project_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + } = {}; + // service + issueService; + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + issues: observable.ref, + // computed + getIssueType: computed, + getIssues: computed, + getIssuesCount: computed, + // actions + fetchIssues: action, + updateIssueStructure: action, + removeIssueFromStructure: action, + deleteIssue: action, + updateGanttIssueStructure: action, + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + + if ( + workspaceSlug && + projectId && + this.rootStore.issueFilter.userFilters && + this.rootStore.issueFilter.userDisplayFilters + ) + this.fetchIssues(workspaceSlug, projectId); + }); + } + + get getIssueType() { + const groupedLayouts = ["kanban", "list", "calendar"]; + const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; + + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null; + if (!issueLayout) return null; + + const _issueState = groupedLayouts.includes(issueLayout) + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : ungroupedLayouts.includes(issueLayout) + ? "ungrouped" + : null; + + return _issueState || null; + } + + get getIssues() { + const projectId: string | null = this.rootStore?.project?.projectId; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + return this.issues?.[projectId]?.[issueType] || null; + } + + get getIssuesCount() { + const issueType = this.getIssueType; + + let issuesCount = 0; + + if (issueType === "grouped") { + const issues = this.getIssues as IIssueGroupedStructure; + + if (!issues) return 0; + + Object.keys(issues).map((group_id) => { + issuesCount += issues[group_id].length; + }); + } + + if (issueType === "groupWithSubGroups") { + const issues = this.getIssues as IIssueGroupWithSubGroupsStructure; + + if (!issues) return 0; + + Object.keys(issues).map((sub_group_id) => { + Object.keys(issues[sub_group_id]).map((group_id) => { + issuesCount += issues[sub_group_id][group_id].length; + }); + }); + } + + if (issueType === "ungrouped") { + const issues = this.getIssues as IIssueUnGroupedStructure; + + if (!issues) return 0; + + issuesCount = issues.length; + } + + return issuesCount; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + const _currentIssueId = issues?.[group_id]?.find((_i) => _i?.id === issue.id); + issues = { + ...issues, + [group_id]: _currentIssueId + ? issues[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) + : [...(issues?.[group_id] ?? []), issue], + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + const _currentIssueId = issues?.[sub_group_id]?.[group_id]?.find((_i) => _i?.id === issue.id); + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: _currentIssueId + ? issues?.[sub_group_id]?.[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) + : [...(issues?.[sub_group_id]?.[group_id] ?? []), issue], + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + const _currentIssueId = issues?.find((_i) => _i?.id === issue.id); + issues = _currentIssueId + ? issues?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) + : [...(issues ?? []), issue]; + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; + }); + }; + + removeIssueFromStructure = (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.getIssueType; + + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: (issues[group_id] ?? []).filter((i) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: (issues[sub_group_id]?.[group_id] ?? []).filter((i) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i) => i?.id !== issue?.id); + } + + runInAction(() => { + this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; + }); + }; + + updateGanttIssueStructure = async (workspaceSlug: string, issue: IIssue, payload: IBlockUpdateData) => { + if (!issue || !workspaceSlug) return; + + const issues = this.getIssues as IIssueUnGroupedStructure; + + const newIssues = issues.map((i) => ({ + ...i, + ...(i.id === issue.id + ? { + ...issue, + sort_order: payload.sort_order?.newSortOrder ?? i.sort_order, + start_date: payload.start_date ?? i.start_date, + target_date: payload.target_date ?? i.target_date, + } + : {}), + })); + + if (payload.sort_order) { + const removedElement = newIssues.splice(payload.sort_order.sourceIndex, 1)[0]; + removedElement.sort_order = payload.sort_order.newSortOrder; + newIssues.splice(payload.sort_order.destinationIndex, 0, removedElement); + } + + runInAction(() => { + this.issues = { + ...this.issues, + [issue.project]: { + ...this.issues[issue.project], + ungrouped: newIssues, + }, + }; + }); + + const newPayload: any = { ...issue, ...payload }; + + if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; + + this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload); + }; + + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i) => i?.id !== issue?.id); + } + + runInAction(() => { + this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; + }); + }; + + fetchIssues = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + this.error = null; + + this.rootStore.workspace.setWorkspaceSlug(workspaceSlug); + this.rootStore.project.setProjectId(projectId); + + const params = this.rootStore?.issueFilter?.appliedFilters; + const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params); + + const issueType = this.getIssueType; + if (issueType != null) { + const _issues = { + ...this.issues, + [projectId]: { + ...this.issues[projectId], + [issueType]: issueResponse, + }, + }; + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + this.loader = false; + this.error = error; + return error; + } + }; +} diff --git a/web/store/issue/issue_calendar_view.store.ts b/web/store/issue/issue_calendar_view.store.ts new file mode 100644 index 00000000000..881e2ee836d --- /dev/null +++ b/web/store/issue/issue_calendar_view.store.ts @@ -0,0 +1,88 @@ +import { action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueType } from "./issue.store"; + +export interface IIssueCalendarViewStore { + // actions + handleDragDrop: (source: any, destination: any) => void; +} + +export class IssueCalendarViewStore implements IIssueCalendarViewStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // actions + handleDragDrop: action, + }); + + this.rootStore = _rootStore; + } + + handleDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.issue.getIssues; + + if (workspaceSlug && projectId && issueType && issueLayout === "calendar" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; + + if (droppableSourceColumnId === droppableDestinationColumnId) return; + + // horizontal + if (droppableSourceColumnId != droppableDestinationColumnId) { + const _sourceIssues = currentIssues[droppableSourceColumnId]; + let _destinationIssues = currentIssues[droppableDestinationColumnId] || []; + + const [removed] = _sourceIssues.splice(source.index, 1); + + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + target_date: droppableDestinationColumnId, + }); + else _destinationIssues = [..._destinationIssues, { ...removed, target_date: droppableDestinationColumnId }]; + + updateIssue = { ...updateIssue, issueId: removed?.id, target_date: droppableDestinationColumnId }; + + currentIssues[droppableSourceColumnId] = _sourceIssues; + currentIssues[droppableDestinationColumnId] = _destinationIssues; + } + + const reorderedIssues = { + ...this.rootStore?.issue.issues, + [projectId]: { + ...this.rootStore?.issue.issues?.[projectId], + [issueType]: { + ...this.rootStore?.issue.issues?.[projectId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.issue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + + return; + }; +} diff --git a/web/store/issue/issue_detail.store.ts b/web/store/issue/issue_detail.store.ts new file mode 100644 index 00000000000..58d4aabe77d --- /dev/null +++ b/web/store/issue/issue_detail.store.ts @@ -0,0 +1,748 @@ +import { observable, action, makeObservable, runInAction, computed } from "mobx"; +// services +import { IssueService, IssueReactionService, IssueCommentService } from "services/issue"; +import { NotificationService } from "services/notification.service"; +// types +import { RootStore } from "../root"; +import { IIssue } from "types"; +// constants +import { groupReactionEmojis } from "constants/issue"; +// uuid +import { v4 as uuidv4 } from "uuid"; + +export interface IIssueDetailStore { + loader: boolean; + error: any | null; + + peekId: string | null; + issues: { + [issueId: string]: IIssue; + }; + issue_reactions: { + [issueId: string]: any; + }; + issue_comments: { + [issueId: string]: any; + }; + issue_comment_reactions: { + [issueId: string]: { + [comment_id: string]: any; + }; + }; + issue_subscription: { + [issueId: string]: any; + }; + + setPeekId: (issueId: string | null) => void; + + // computed + getIssue: IIssue | null; + getIssueReactions: any | null; + getIssueComments: any | null; + getIssueCommentReactions: any | null; + getIssueSubscription: any | null; + + // fetch issue details + fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + // creating issue + createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + optimisticallyCreateIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + // updating issue + updateIssue: (workspaceId: string, projectId: string, issueId: string, data: Partial) => Promise; + // deleting issue + deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + + fetchPeekIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + + fetchIssueReactions: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + createIssueReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise; + removeIssueReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise; + + fetchIssueComments: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + createIssueComment: (workspaceSlug: string, projectId: string, issueId: string, data: any) => Promise; + updateIssueComment: ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + data: any + ) => Promise; + removeIssueComment: (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => Promise; + + fetchIssueCommentReactions: ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string + ) => Promise; + creationIssueCommentReaction: ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + reaction: string + ) => Promise; + removeIssueCommentReaction: ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + reaction: string + ) => Promise; + + fetchIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + createIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + removeIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; +} + +export class IssueDetailStore implements IIssueDetailStore { + loader: boolean = false; + error: any | null = null; + + peekId: string | null = null; + issues: { + [issueId: string]: IIssue; + } = {}; + issue_reactions: { + [issueId: string]: any; + } = {}; + issue_comments: { + [issueId: string]: any; + } = {}; + issue_comment_reactions: { + [issueId: string]: any; + } = {}; + issue_subscription: { + [issueId: string]: any; + } = {}; + + // root store + rootStore; + // service + issueService; + issueReactionService; + issueCommentService; + notificationService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + + peekId: observable.ref, + issues: observable.ref, + issue_reactions: observable.ref, + issue_comments: observable.ref, + issue_comment_reactions: observable.ref, + issue_subscription: observable.ref, + + getIssue: computed, + getIssueReactions: computed, + getIssueComments: computed, + getIssueCommentReactions: computed, + getIssueSubscription: computed, + + setPeekId: action, + + fetchIssueDetails: action, + createIssue: action, + optimisticallyCreateIssue: action, + updateIssue: action, + deleteIssue: action, + + fetchPeekIssueDetails: action, + + fetchIssueReactions: action, + createIssueReaction: action, + removeIssueReaction: action, + + fetchIssueComments: action, + createIssueComment: action, + updateIssueComment: action, + removeIssueComment: action, + + fetchIssueCommentReactions: action, + creationIssueCommentReaction: action, + removeIssueCommentReaction: action, + + fetchIssueSubscription: action, + createIssueSubscription: action, + removeIssueSubscription: action, + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + this.issueReactionService = new IssueReactionService(); + this.issueCommentService = new IssueCommentService(); + this.notificationService = new NotificationService(); + } + + get getIssue() { + if (!this.peekId) return null; + const _issue = this.issues[this.peekId]; + return _issue || null; + } + + get getIssueReactions() { + if (!this.peekId) return null; + const _reactions = this.issue_reactions[this.peekId]; + return _reactions || null; + } + + get getIssueComments() { + if (!this.peekId) return null; + const _comments = this.issue_comments[this.peekId]; + return _comments || null; + } + + get getIssueCommentReactions() { + if (!this.peekId) return null; + const _commentReactions = this.issue_comment_reactions[this.peekId]; + return _commentReactions || null; + } + + get getIssueSubscription() { + if (!this.peekId) return null; + const _commentSubscription = this.issue_subscription[this.peekId]; + return _commentSubscription || null; + } + + setPeekId = (issueId: string | null) => (this.peekId = issueId); + + fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + this.loader = true; + this.error = null; + this.peekId = issueId; + + const issueDetailsResponse = await this.issueService.retrieve(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.loader = false; + this.error = null; + this.issues = { + ...this.issues, + [issueId]: issueDetailsResponse, + }; + }); + + return issueDetailsResponse; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + optimisticallyCreateIssue = async (workspaceSlug: string, projectId: string, data: Partial) => { + const tempId = data?.id || uuidv4(); + + runInAction(() => { + this.loader = true; + this.error = null; + this.issues = { + ...this.issues, + [tempId]: data as IIssue, + }; + }); + + try { + const response = await this.issueService.createIssue( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser! + ); + + runInAction(() => { + this.loader = false; + this.error = null; + this.issues = { + ...this.issues, + [response.id]: response, + }; + }); + + return response; + } catch (error) { + this.loader = false; + this.error = error; + + throw error; + } + }; + + createIssue = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const user = this.rootStore.user.currentUser ?? undefined; + + const response = await this.issueService.createIssue(workspaceSlug, projectId, data, user); + + runInAction(() => { + this.loader = false; + this.error = null; + this.issues = { + ...this.issues, + [response.id]: response, + }; + }); + + return response; + } catch (error) { + this.loader = false; + this.error = error; + + throw error; + } + }; + + updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + const newIssues = { ...this.issues }; + newIssues[issueId] = { + ...newIssues[issueId], + ...data, + }; + + try { + runInAction(() => { + this.loader = true; + this.error = null; + this.issues = newIssues; + }); + + const user = this.rootStore.user.currentUser; + + if (!user) return; + + const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data, user); + + runInAction(() => { + this.loader = false; + this.error = null; + this.issues = { + ...this.issues, + [issueId]: { + ...this.issues[issueId], + ...response, + }, + }; + }); + + return response; + } catch (error) { + this.fetchIssueDetails(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + return error; + } + }; + + deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + const newIssues = { ...this.issues }; + delete newIssues[issueId]; + + try { + runInAction(() => { + this.loader = true; + this.error = null; + this.issues = newIssues; + }); + + const user = this.rootStore.user.currentUser; + + if (!user) return; + + const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId, user); + + runInAction(() => { + this.loader = false; + this.error = null; + }); + + return response; + } catch (error) { + this.fetchIssueDetails(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + return error; + } + }; + + fetchPeekIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + this.loader = true; + this.error = null; + + this.peekId = issueId; + + const issueDetailsResponse = await this.issueService.retrieve(workspaceSlug, projectId, issueId); + await this.fetchIssueReactions(workspaceSlug, projectId, issueId); + await this.fetchIssueComments(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.loader = false; + this.error = null; + this.issues = { + ...this.issues, + [issueId]: issueDetailsResponse, + }; + }); + + return issueDetailsResponse; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + // reactions + fetchIssueReactions = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + const _reactions = await this.issueReactionService.listIssueReactions(workspaceSlug, projectId, issueId); + + const _issue_reactions = { + ...this.issue_reactions, + [issueId]: groupReactionEmojis(_reactions), + }; + + runInAction(() => { + this.issue_reactions = _issue_reactions; + }); + } catch (error) { + console.warn("error creating the issue reaction", error); + throw error; + } + }; + createIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => { + let _currentReactions = this.getIssueReactions; + + try { + const _reaction = await this.issueReactionService.createIssueReaction(workspaceSlug, projectId, issueId, { + reaction, + }); + + _currentReactions = { + ..._currentReactions, + [reaction]: [..._currentReactions[reaction], { ..._reaction }], + }; + + runInAction(() => { + this.issue_reactions = { + ...this.issue_reactions, + [issueId]: _currentReactions, + }; + }); + } catch (error) { + runInAction(() => { + this.issue_reactions = { + ...this.issue_reactions, + [issueId]: _currentReactions, + }; + }); + console.warn("error creating the issue reaction", error); + throw error; + } + }; + removeIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => { + let _currentReactions = this.getIssueReactions; + + try { + const user = this.rootStore.user.currentUser; + + if (user) { + _currentReactions = { + ..._currentReactions, + [reaction]: [..._currentReactions[reaction].filter((r: any) => r.actor !== user.id)], + }; + + runInAction(() => { + this.issue_reactions = { + ...this.issue_reactions, + [issueId]: _currentReactions, + }; + }); + + await this.issueReactionService.deleteIssueReaction(workspaceSlug, projectId, issueId, reaction); + } + } catch (error) { + runInAction(() => { + this.issue_reactions = { + ...this.issue_reactions, + [issueId]: _currentReactions, + }; + }); + console.warn("error removing the issue reaction", error); + throw error; + } + }; + + // comments + fetchIssueComments = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + const _issueCommentResponse = await this.issueService.getIssueActivities(workspaceSlug, projectId, issueId); + + const _issueComments = { + ...this.issue_comments, + [issueId]: [..._issueCommentResponse], + }; + + runInAction(() => { + this.issue_comments = _issueComments; + }); + } catch (error) { + console.warn("error creating the issue comment", error); + throw error; + } + }; + createIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, data: any) => { + try { + const _issueCommentResponse = await this.issueCommentService.createIssueComment( + workspaceSlug, + projectId, + issueId, + data, + undefined + ); + + const _issueComments = { + ...this.issue_comments, + [issueId]: [...this.issue_comments[issueId], _issueCommentResponse], + }; + + runInAction(() => { + this.issue_comments = _issueComments; + }); + } catch (error) { + console.warn("error creating the issue comment", error); + throw error; + } + }; + updateIssueComment = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + data: any + ) => { + try { + const _issueCommentResponse = await this.issueCommentService.patchIssueComment( + workspaceSlug, + projectId, + issueId, + commentId, + data, + undefined + ); + + const _issueComments = { + ...this.issue_comments, + [issueId]: this.issue_comments[issueId].map((comment: any) => + comment.id === commentId ? _issueCommentResponse : comment + ), + }; + + runInAction(() => { + this.issue_comments = _issueComments; + }); + } catch (error) { + console.warn("error updating the issue comment", error); + throw error; + } + }; + removeIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => { + try { + const _issueComments = { + ...this.issue_comments, + [issueId]: this.issue_comments[issueId].filter((comment: any) => comment.id != commentId), + }; + + await this.issueCommentService.deleteIssueComment(workspaceSlug, projectId, issueId, commentId, undefined); + + runInAction(() => { + this.issue_comments = _issueComments; + }); + } catch (error) { + console.warn("error removing the issue comment", error); + throw error; + } + }; + + // comment reaction + fetchIssueCommentReactions = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => { + try { + const _reactions = await this.issueReactionService.listIssueCommentReactions(workspaceSlug, projectId, commentId); + + const _issue_comment_reactions = { + ...this.issue_comment_reactions, + [issueId]: { + ...this.issue_comment_reactions[issueId], + [commentId]: groupReactionEmojis(_reactions), + }, + }; + + runInAction(() => { + this.issue_comment_reactions = _issue_comment_reactions; + }); + } catch (error) { + console.warn("error removing the issue comment", error); + throw error; + } + }; + creationIssueCommentReaction = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + reaction: string + ) => { + let _currentReactions = this.getIssueCommentReactions; + _currentReactions = _currentReactions && commentId ? _currentReactions?.[commentId] : null; + + try { + const _reaction = await this.issueReactionService.createIssueCommentReaction( + workspaceSlug, + projectId, + commentId, + { + reaction, + } + ); + + _currentReactions = { + ..._currentReactions, + [reaction]: [..._currentReactions?.[reaction], { ..._reaction }], + }; + + const _issue_comment_reactions = { + ...this.issue_comment_reactions, + [issueId]: { + ...this.issue_comment_reactions[issueId], + [commentId]: _currentReactions, + }, + }; + + runInAction(() => { + this.issue_comment_reactions = _issue_comment_reactions; + }); + } catch (error) { + console.warn("error removing the issue comment", error); + throw error; + } + }; + removeIssueCommentReaction = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + reaction: string + ) => { + let _currentReactions = this.getIssueCommentReactions; + _currentReactions = _currentReactions && commentId ? _currentReactions?.[commentId] : null; + + try { + const user = this.rootStore.user.currentUser; + + if (user) { + _currentReactions = { + ..._currentReactions, + [reaction]: [..._currentReactions?.[reaction].filter((r: any) => r.actor !== user.id)], + }; + + const _issue_comment_reactions = { + ...this.issue_comment_reactions, + [issueId]: { + ...this.issue_comment_reactions[issueId], + [commentId]: _currentReactions, + }, + }; + + runInAction(() => { + this.issue_comment_reactions = _issue_comment_reactions; + }); + + await this.issueReactionService.deleteIssueCommentReaction(workspaceSlug, projectId, commentId, reaction); + } + } catch (error) { + console.warn("error removing the issue comment", error); + throw error; + } + }; + + // subscription + fetchIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + const _subscription = await this.notificationService.getIssueNotificationSubscriptionStatus( + workspaceSlug, + projectId, + issueId + ); + + const _issue_subscription = { + ...this.issue_subscription, + [issueId]: _subscription, + }; + + runInAction(() => { + this.issue_subscription = _issue_subscription; + }); + } catch (error) { + console.warn("error fetching the issue subscription", error); + throw error; + } + }; + createIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId); + + const _issue_subscription = { + ...this.issue_subscription, + [issueId]: { subscribed: true }, + }; + + runInAction(() => { + this.issue_subscription = _issue_subscription; + }); + } catch (error) { + console.warn("error creating the issue subscription", error); + throw error; + } + }; + removeIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + const _issue_subscription = { + ...this.issue_subscription, + [issueId]: { subscribed: false }, + }; + + runInAction(() => { + this.issue_subscription = _issue_subscription; + }); + + await this.notificationService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId); + } catch (error) { + console.warn("error removing the issue subscription", error); + throw error; + } + }; +} diff --git a/web/store/issue/issue_draft.store.ts b/web/store/issue/issue_draft.store.ts new file mode 100644 index 00000000000..76faacb7ed7 --- /dev/null +++ b/web/store/issue/issue_draft.store.ts @@ -0,0 +1,169 @@ +// mobx +import { action, observable, runInAction, makeAutoObservable } from "mobx"; +// services +import { IssueDraftService } from "services/issue"; +// types +import type { IIssue, IUser } from "types"; + +export class DraftIssuesStore { + issues: { [key: string]: IIssue } = {}; + isIssuesLoading: boolean = false; + rootStore: any | null = null; + issueDraftService; + + constructor(_rootStore: any | null = null) { + makeAutoObservable(this, { + issues: observable.ref, + isIssuesLoading: observable.ref, + rootStore: observable.ref, + loadDraftIssues: action, + getIssueById: action, + createDraftIssue: action, + updateDraftIssue: action, + deleteDraftIssue: action, + }); + + this.rootStore = _rootStore; + this.issueDraftService = new IssueDraftService(); + } + + /** + * @description Fetch all draft issues of a project and hydrate issues field + */ + + loadDraftIssues = async (workspaceSlug: string, projectId: string, params?: any) => { + this.isIssuesLoading = true; + try { + const issuesResponse = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params); + + const issues = Array.isArray(issuesResponse) ? { allIssues: issuesResponse } : issuesResponse; + + runInAction(() => { + this.issues = issues; + this.isIssuesLoading = false; + }); + } catch (error) { + this.isIssuesLoading = false; + console.error("Fetching issues error", error); + } + }; + + /** + * @description Fetch a single draft issue by id and hydrate issues field + * @param workspaceSlug + * @param projectId + * @param issueId + * @returns {IIssue} + */ + + getIssueById = async (workspaceSlug: string, projectId: string, issueId: string): Promise => { + if (this.issues[issueId]) return this.issues[issueId]; + + try { + const issueResponse: IIssue = await this.issueDraftService.getDraftIssueById(workspaceSlug, projectId, issueId); + + const issues = { + ...this.issues, + [issueId]: { ...issueResponse }, + }; + + runInAction(() => { + this.issues = issues; + }); + + return issueResponse; + } catch (error) { + throw error; + } + }; + + /** + * @description Create a new draft issue and hydrate issues field + * @param workspaceSlug + * @param projectId + * @param issueForm + * @param user + * @returns {IIssue} + */ + + createDraftIssue = async ( + workspaceSlug: string, + projectId: string, + issueForm: IIssue, + user: IUser + ): Promise => { + try { + const issueResponse = await this.issueDraftService.createDraftIssue(workspaceSlug, projectId, issueForm); + + const issues = { + ...this.issues, + [issueResponse.id]: { ...issueResponse }, + }; + + runInAction(() => { + this.issues = issues; + }); + return issueResponse; + } catch (error) { + console.error("Creating issue error", error); + throw error; + } + }; + + updateDraftIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + issueForm: Partial, + user: IUser + ) => { + // keep a copy of the issue in the store + const originalIssue = { ...this.issues[issueId] }; + + // immediately update the issue in the store + const updatedIssue = { ...this.issues[issueId], ...issueForm }; + + try { + runInAction(() => { + this.issues[issueId] = { ...updatedIssue }; + }); + + // make a patch request to update the issue + const issueResponse: IIssue = await this.issueDraftService.updateDraftIssue( + workspaceSlug, + projectId, + issueId, + issueForm + ); + + const updatedIssues = { ...this.issues }; + updatedIssues[issueId] = { ...issueResponse }; + + runInAction(() => { + this.issues = updatedIssues; + }); + } catch (error) { + // if there is an error, revert the changes + runInAction(() => { + this.issues[issueId] = originalIssue; + }); + + return error; + } + }; + + deleteDraftIssue = async (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => { + const issues = { ...this.issues }; + delete issues[issueId]; + + try { + runInAction(() => { + this.issues = issues; + }); + + this.issueDraftService.deleteDraftIssue(workspaceSlug, projectId, issueId); + } catch (error) { + console.error("Deleting issue error", error); + } + }; +} diff --git a/web/store/issue/issue_filters.store.ts b/web/store/issue/issue_filters.store.ts new file mode 100644 index 00000000000..be28cca87d1 --- /dev/null +++ b/web/store/issue/issue_filters.store.ts @@ -0,0 +1,247 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// services +import { ProjectService } from "services/project"; +import { IssueService } from "services/issue"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + IProjectViewProps, + TIssueParams, +} from "types"; + +export interface IIssueFilterStore { + loader: boolean; + error: any | null; + // TODO: store filters and properties separately for each project + userDisplayProperties: IIssueDisplayProperties; + userDisplayFilters: IIssueDisplayFilterOptions; + userFilters: IIssueFilterOptions; + defaultDisplayFilters: IIssueDisplayFilterOptions; + defaultFilters: IIssueFilterOptions; + + // action + fetchUserProjectFilters: (workspaceSlug: string, projectId: string) => Promise; + updateUserFilters: ( + workspaceSlug: string, + projectId: string, + filterToUpdate: Partial + ) => Promise; + updateDisplayProperties: ( + workspaceSlug: string, + projectId: string, + properties: Partial + ) => Promise; + + // computed + appliedFilters: TIssueParams[] | null; +} + +export class IssueFilterStore implements IIssueFilterStore { + loader: boolean = false; + error: any | null = null; + + // observables + userDisplayProperties: IIssueDisplayProperties = {}; + userDisplayFilters: IIssueDisplayFilterOptions = {}; + userFilters: IIssueFilterOptions = {}; + defaultDisplayFilters: IIssueDisplayFilterOptions = {}; + defaultFilters: IIssueFilterOptions = {}; + defaultDisplayProperties: IIssueDisplayProperties = { + assignee: true, + start_date: true, + due_date: true, + labels: true, + key: true, + priority: true, + state: true, + sub_issue_count: true, + link: true, + attachment_count: true, + estimate: true, + created_on: true, + updated_on: true, + }; + + // root store + rootStore; + + // services + projectService; + issueService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + loader: observable.ref, + error: observable.ref, + + // observables + defaultDisplayFilters: observable.ref, + defaultFilters: observable.ref, + userDisplayProperties: observable.ref, + userDisplayFilters: observable.ref, + userFilters: observable.ref, + + // actions + fetchUserProjectFilters: action, + updateUserFilters: action, + updateDisplayProperties: action, + + // computed + appliedFilters: computed, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.issueService = new IssueService(); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + if (!this.userFilters || !this.userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.userFilters?.priority || undefined, + state_group: this.userFilters?.state_group || undefined, + state: this.userFilters?.state || undefined, + assignees: this.userFilters?.assignees || undefined, + mentions: this.userFilters?.mentions || undefined, + created_by: this.userFilters?.created_by || undefined, + labels: this.userFilters?.labels || undefined, + start_date: this.userFilters?.start_date || undefined, + target_date: this.userFilters?.target_date || undefined, + group_by: this.userDisplayFilters?.group_by || undefined, + order_by: this.userDisplayFilters?.order_by || "-created_at", + sub_group_by: this.userDisplayFilters?.sub_group_by || undefined, + type: this.userDisplayFilters?.type || undefined, + sub_issue: this.userDisplayFilters?.sub_issue || true, + show_empty_groups: this.userDisplayFilters?.show_empty_groups || true, + start_target_date: this.userDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (this.userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (this.userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + fetchUserProjectFilters = async (workspaceSlug: string, projectId: string) => { + try { + const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId); + const issueProperties = await this.issueService.getIssueDisplayProperties(workspaceSlug, projectId); + + runInAction(() => { + this.userFilters = memberResponse?.view_props?.filters; + this.userDisplayFilters = { + ...memberResponse?.view_props?.display_filters, + // add calendar display filters if not already present + calendar: { + show_weekends: memberResponse?.view_props?.display_filters?.calendar?.show_weekends || true, + layout: memberResponse?.view_props?.display_filters?.calendar?.layout || "month", + }, + }; + this.userDisplayProperties = issueProperties?.properties || this.defaultDisplayProperties; + // default props from api + this.defaultFilters = memberResponse.default_props.filters; + this.defaultDisplayFilters = memberResponse.default_props.display_filters ?? {}; + }); + } catch (error) { + runInAction(() => { + this.error = error; + }); + + console.log("Failed to fetch user filters in issue filter store", error); + } + }; + + updateUserFilters = async (workspaceSlug: string, projectId: string, filterToUpdate: Partial) => { + const newViewProps = { + display_filters: { + ...this.userDisplayFilters, + ...filterToUpdate.display_filters, + }, + filters: { + ...this.userFilters, + ...filterToUpdate.filters, + }, + }; + + // set sub_group_by to null if group_by is set to null + if (newViewProps.display_filters.group_by === null) newViewProps.display_filters.sub_group_by = null; + + // set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same + if ( + newViewProps.display_filters.layout === "kanban" && + newViewProps.display_filters.group_by === newViewProps.display_filters.sub_group_by + ) + newViewProps.display_filters.sub_group_by = null; + + // set group_by to state if layout is switched to kanban and group_by is null + if (newViewProps.display_filters.layout === "kanban" && newViewProps.display_filters.group_by === null) + newViewProps.display_filters.group_by = "state"; + + try { + runInAction(() => { + this.userFilters = newViewProps.filters; + this.userDisplayFilters = newViewProps.display_filters; + }); + + this.projectService.setProjectView(workspaceSlug, projectId, { + view_props: newViewProps, + }); + } catch (error) { + this.fetchUserProjectFilters(workspaceSlug, projectId); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user filters in issue filter store", error); + } + }; + + updateDisplayProperties = async ( + workspaceSlug: string, + projectId: string, + properties: Partial + ) => { + const newProperties: IIssueDisplayProperties = { + ...this.userDisplayProperties, + ...properties, + }; + + try { + runInAction(() => { + this.userDisplayProperties = newProperties; + }); + + await this.issueService.updateIssueDisplayProperties(workspaceSlug, projectId, newProperties); + } catch (error) { + this.fetchUserProjectFilters(workspaceSlug, projectId); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user display properties in issue filter store", error); + } + }; +} diff --git a/web/store/issue/issue_kanban_view.store.ts b/web/store/issue/issue_kanban_view.store.ts new file mode 100644 index 00000000000..827972694ea --- /dev/null +++ b/web/store/issue/issue_kanban_view.store.ts @@ -0,0 +1,448 @@ +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueType } from "./issue.store"; + +export interface IIssueKanBanViewStore { + kanBanToggle: { + groupByHeaderMinMax: string[]; + subgroupByIssuesVisibility: string[]; + }; + // computed + canUserDragDrop: boolean; + canUserDragDropVertically: boolean; + canUserDragDropHorizontally: boolean; + // actions + handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void; + handleSwimlaneDragDrop: (source: any, destination: any) => void; + handleDragDrop: (source: any, destination: any) => void; +} + +export class IssueKanBanViewStore implements IIssueKanBanViewStore { + kanBanToggle: { + groupByHeaderMinMax: string[]; + subgroupByIssuesVisibility: string[]; + } = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] }; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + kanBanToggle: observable, + // computed + canUserDragDrop: computed, + canUserDragDropVertically: computed, + canUserDragDropHorizontally: computed, + + // actions + handleKanBanToggle: action, + handleSwimlaneDragDrop: action, + handleDragDrop: action, + }); + + this.rootStore = _rootStore; + } + + get canUserDragDrop() { + if (this.rootStore.issueDetail.peekId) return false; + if ( + this.rootStore?.issueFilter?.userDisplayFilters?.order_by && + this.rootStore?.issueFilter?.userDisplayFilters?.order_by === "sort_order" && + this.rootStore?.issueFilter?.userDisplayFilters?.group_by && + ["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.group_by) + ) { + if (!this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by) return true; + if ( + this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by && + ["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by) + ) + return true; + } + return false; + } + + get canUserDragDropVertically() { + return false; + } + + get canUserDragDropHorizontally() { + return false; + } + + handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { + this.kanBanToggle = { + ...this.kanBanToggle, + [toggle]: this.kanBanToggle[toggle].includes(value) + ? this.kanBanToggle[toggle].filter((v) => v !== value) + : [...this.kanBanToggle[toggle], value], + }; + }; + + handleSwimlaneDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.issue.getIssues; + + const sortOrderDefaultValue = 65535; + + if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + // source, destination group and sub group id + let droppableSourceColumnId = source?.droppableId || null; + droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; + let droppableDestinationColumnId = destination?.droppableId || null; + droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; + if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; + + const source_group_id: string = droppableSourceColumnId[0]; + const source_sub_group_id: string = droppableSourceColumnId[1] === "null" ? null : droppableSourceColumnId[1]; + + const destination_group_id: string = droppableDestinationColumnId[0]; + const destination_sub_group_id: string = + droppableDestinationColumnId[1] === "null" ? null : droppableDestinationColumnId[1]; + + if (source_sub_group_id === destination_sub_group_id) { + if (source_group_id === destination_group_id) { + const _issues = currentIssues[source_sub_group_id][source_group_id]; + + // update the sort order + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _issues.length - 1) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2, + }; + } + + const [removed] = _issues.splice(source.index, 1); + _issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order }); + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_sub_group_id][source_group_id] = _issues; + } + + if (source_group_id != destination_group_id) { + const _sourceIssues = currentIssues[source_sub_group_id][source_group_id]; + let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + let issueStatePriority = {}; + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") { + updateIssue = { ...updateIssue, state: destination_group_id }; + issueStatePriority = { ...issueStatePriority, state: destination_group_id }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") { + updateIssue = { ...updateIssue, priority: destination_group_id }; + issueStatePriority = { ...issueStatePriority, priority: destination_group_id }; + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + ...issueStatePriority, + }); + else + _destinationIssues = [ + ..._destinationIssues, + { ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority }, + ]; + updateIssue = { ...updateIssue, issueId: removed?.id }; + + currentIssues[source_sub_group_id][source_group_id] = _sourceIssues; + currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues; + } + } + + if (source_sub_group_id != destination_sub_group_id) { + const _sourceIssues = currentIssues[source_sub_group_id][source_group_id]; + let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + let issueStatePriority = {}; + if (source_group_id === destination_group_id) { + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "state") { + updateIssue = { ...updateIssue, state: destination_sub_group_id }; + issueStatePriority = { ...issueStatePriority, state: destination_sub_group_id }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "priority") { + updateIssue = { ...updateIssue, priority: destination_sub_group_id }; + issueStatePriority = { ...issueStatePriority, priority: destination_sub_group_id }; + } + } else { + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "state") { + updateIssue = { ...updateIssue, state: destination_sub_group_id, priority: destination_group_id }; + issueStatePriority = { + ...issueStatePriority, + state: destination_sub_group_id, + priority: destination_group_id, + }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "priority") { + updateIssue = { ...updateIssue, state: destination_group_id, priority: destination_sub_group_id }; + issueStatePriority = { + ...issueStatePriority, + state: destination_group_id, + priority: destination_sub_group_id, + }; + } + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + ...issueStatePriority, + }); + else + _destinationIssues = [ + ..._destinationIssues, + { ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority }, + ]; + + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_sub_group_id][source_group_id] = _sourceIssues; + currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues; + } + + const reorderedIssues = { + ...this.rootStore?.issue.issues, + [projectId]: { + ...this.rootStore?.issue.issues?.[projectId], + [issueType]: { + ...this.rootStore?.issue.issues?.[projectId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.issue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + }; + + handleDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.issue.getIssues; + + const sortOrderDefaultValue = 65535; + + if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + // source, destination group and sub group id + let droppableSourceColumnId = source?.droppableId || null; + droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; + let droppableDestinationColumnId = destination?.droppableId || null; + droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; + if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; + + const source_group_id: string = droppableSourceColumnId[0]; + const destination_group_id: string = droppableDestinationColumnId[0]; + + if (this.canUserDragDrop) { + // vertical + if (source_group_id === destination_group_id) { + const _issues = currentIssues[source_group_id]; + + // update the sort order + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _issues.length - 1) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2, + }; + } + + const [removed] = _issues.splice(source.index, 1); + _issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order }); + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_group_id] = _issues; + } + + // horizontal + if (source_group_id != destination_group_id) { + const _sourceIssues = currentIssues[source_group_id]; + let _destinationIssues = currentIssues[destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + let issueStatePriority = {}; + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") { + updateIssue = { ...updateIssue, state: destination_group_id }; + issueStatePriority = { ...issueStatePriority, state: destination_group_id }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") { + updateIssue = { ...updateIssue, priority: destination_group_id }; + issueStatePriority = { ...issueStatePriority, priority: destination_group_id }; + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + ...issueStatePriority, + }); + else + _destinationIssues = [ + ..._destinationIssues, + { ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority }, + ]; + updateIssue = { ...updateIssue, issueId: removed?.id }; + + currentIssues[source_group_id] = _sourceIssues; + currentIssues[destination_group_id] = _destinationIssues; + } + } + + // user can drag the issues only vertically + if (this.canUserDragDropVertically && destination_group_id === destination_group_id) { + } + + // user can drag the issues only horizontally + if (this.canUserDragDropHorizontally && destination_group_id != destination_group_id) { + } + + const reorderedIssues = { + ...this.rootStore?.issue.issues, + [projectId]: { + ...this.rootStore?.issue.issues?.[projectId], + [issueType]: { + ...this.rootStore?.issue.issues?.[projectId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.issue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + }; +} diff --git a/web/store/issue/issue_quick_add.store.ts b/web/store/issue/issue_quick_add.store.ts new file mode 100644 index 00000000000..44683b57882 --- /dev/null +++ b/web/store/issue/issue_quick_add.store.ts @@ -0,0 +1,227 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// services +import { IssueService } from "services/issue"; +// types +import { RootStore } from "../root"; +import { IIssue } from "types"; +// uuid +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; +import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "./issue.store"; + +export interface IIssueQuickAddStore { + loader: boolean; + error: any | null; + + createIssue: ( + workspaceSlug: string, + projectId: string, + grouping: { + group_id: string | null; + sub_group_id: string | null; + }, + data: Partial + ) => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + updateQuickAddIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; +} + +export class IssueQuickAddStore implements IIssueQuickAddStore { + loader: boolean = false; + error: any | null = null; + + // root store + rootStore; + // service + issueService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + + createIssue: action, + updateIssueStructure: action, + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + } + + createIssue = async ( + workspaceSlug: string, + projectId: string, + grouping: { + group_id: string | null; + sub_group_id: string | null; + }, + data: Partial + ) => { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const { group_id, sub_group_id } = grouping; + + try { + this.updateIssueStructure(group_id, sub_group_id, data as IIssue); + + const response = await this.issueService.createIssue( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser! + ); + + this.updateQuickAddIssueStructure(group_id, sub_group_id, { + ...data, + ...response, + }); + + runInAction(() => { + this.loader = false; + this.error = null; + }); + + return response; + } catch (error) { + this.loader = false; + this.error = error; + + throw error; + } + }; + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.rootStore.issue.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.rootStore.issue.getIssues; + if (!issues) return null; + + if (group_id === "null") group_id = null; + if (sub_group_id === "null") sub_group_id = null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + const _currentIssueId = issues?.[group_id]?.find((_i) => _i?.id === issue.id); + issues = { + ...issues, + [group_id]: _currentIssueId + ? issues[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) + : [...(issues?.[group_id] ?? []), issue], + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + const _currentIssueId = issues?.[sub_group_id]?.[group_id]?.find((_i) => _i?.id === issue.id); + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: _currentIssueId + ? issues?.[sub_group_id]?.[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) + : [...(issues?.[sub_group_id]?.[group_id] ?? []), issue], + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + const _currentIssueId = issues?.find((_i) => _i?.id === issue.id); + issues = _currentIssueId + ? issues?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) + : [...(issues ?? []), issue]; + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.rootStore.issue.issues = { + ...this.rootStore.issue.issues, + [projectId]: { ...this.rootStore.issue.issues[projectId], [issueType]: issues }, + }; + }); + }; + + // same as above function but will use temp id instead of real id + updateQuickAddIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.rootStore.issue.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.rootStore.issue.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + const _currentIssueId = issues?.[group_id]?.find((_i) => _i?.tempId === issue.tempId); + issues = { + ...issues, + [group_id]: _currentIssueId + ? issues[group_id]?.map((i: IIssue) => + i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i + ) + : [...(issues?.[group_id] ?? []), issue], + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + const _currentIssueId = issues?.[sub_group_id]?.[group_id]?.find((_i) => _i?.tempId === issue.tempId); + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: _currentIssueId + ? issues?.[sub_group_id]?.[group_id]?.map((i: IIssue) => + i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i + ) + : [...(issues?.[sub_group_id]?.[group_id] ?? []), issue], + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + const _currentIssueId = issues?.find((_i) => _i?.tempId === issue.tempId); + issues = _currentIssueId + ? issues?.map((i: IIssue) => (i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i)) + : [...(issues ?? []), issue]; + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.rootStore.issue.issues = { + ...this.rootStore.issue.issues, + [projectId]: { ...this.rootStore.issue.issues[projectId], [issueType]: issues }, + }; + }); + }; +} diff --git a/web/store/module/index.ts b/web/store/module/index.ts new file mode 100644 index 00000000000..3d62d3de815 --- /dev/null +++ b/web/store/module/index.ts @@ -0,0 +1,5 @@ +export * from "./module_filters.store"; +export * from "./module_issue_kanban_view.store"; +export * from "./module_issue_calendar_view.store"; +export * from "./module_issue.store"; +export * from "./modules.store"; diff --git a/web/store/module/module_filters.store.ts b/web/store/module/module_filters.store.ts new file mode 100644 index 00000000000..ae94af59e84 --- /dev/null +++ b/web/store/module/module_filters.store.ts @@ -0,0 +1,178 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// services +import { ProjectService } from "services/project"; +import { ModuleService } from "services/module.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssueFilterOptions, IModule, TIssueParams } from "types"; + +export interface IModuleFilterStore { + loader: boolean; + error: any | null; + moduleFilters: IIssueFilterOptions; + defaultFilters: IIssueFilterOptions; + + // action + fetchModuleFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + updateModuleFilters: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + filterToUpdate: Partial + ) => Promise; + + // computed + appliedFilters: TIssueParams[] | null; +} + +export class ModuleFilterStore implements IModuleFilterStore { + loader: boolean = false; + error: any | null = null; + + // observables + moduleFilters: IIssueFilterOptions = {}; + defaultFilters: IIssueFilterOptions = {}; + + // root store + rootStore; + + // services + projectService; + moduleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + loader: observable.ref, + error: observable.ref, + + // observables + defaultFilters: observable.ref, + moduleFilters: observable.ref, + + // actions + fetchModuleFilters: action, + updateModuleFilters: action, + + // computed + appliedFilters: computed, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.moduleService = new ModuleService(); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + const userDisplayFilters = this.rootStore.issueFilter.userDisplayFilters; + + if (!this.moduleFilters || !userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.moduleFilters?.priority || undefined, + state_group: this.moduleFilters?.state_group || undefined, + state: this.moduleFilters?.state || undefined, + assignees: this.moduleFilters?.assignees || undefined, + created_by: this.moduleFilters?.created_by || undefined, + labels: this.moduleFilters?.labels || undefined, + start_date: this.moduleFilters?.start_date || undefined, + target_date: this.moduleFilters?.target_date || undefined, + group_by: userDisplayFilters?.group_by || "state", + order_by: userDisplayFilters?.order_by || "-created_at", + sub_group_by: userDisplayFilters?.sub_group_by || undefined, + type: userDisplayFilters?.type || undefined, + sub_issue: userDisplayFilters?.sub_issue || true, + show_empty_groups: userDisplayFilters?.show_empty_groups || true, + start_target_date: userDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(userDisplayFilters.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + fetchModuleFilters = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const response = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.moduleFilters = response.view_props?.filters ?? {}; + this.loader = false; + this.error = null; + }); + + return response; + } catch (error) { + console.error("Failed to fetch module details in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + updateModuleFilters = async ( + workspaceSlug: string, + projectId: string, + moduleId: string, + filterToUpdate: Partial + ) => { + const newFilters = { + ...this.moduleFilters, + ...filterToUpdate, + }; + + try { + runInAction(() => { + this.moduleFilters = newFilters; + }); + + const user = this.rootStore.user.currentUser ?? undefined; + + this.moduleService.patchModule( + workspaceSlug, + projectId, + moduleId, + { + view_props: { + filters: newFilters, + }, + }, + user + ); + } catch (error) { + this.fetchModuleFilters(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user filters in issue filter store", error); + } + }; +} diff --git a/web/store/module/module_issue.store.ts b/web/store/module/module_issue.store.ts new file mode 100644 index 00000000000..165b51b622d --- /dev/null +++ b/web/store/module/module_issue.store.ts @@ -0,0 +1,379 @@ +import { observable, action, computed, makeObservable, runInAction, autorun } from "mobx"; +// store +import { RootStore } from "../root"; +// services +import { ModuleService } from "services/module.service"; +// helpers +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; +// types +import { IIssue } from "types"; +import { IBlockUpdateData } from "components/gantt-chart"; +import { + IIssueGroupWithSubGroupsStructure, + IIssueGroupedStructure, + IIssueType, + IIssueUnGroupedStructure, +} from "store/issue"; + +export interface IModuleIssueStore { + loader: boolean; + error: any | null; + // issues + issues: { + [module_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + // computed + getIssueType: IIssueType | null; + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + getIssuesCount: number; + // action + fetchIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + updateGanttIssueStructure: ( + workspaceSlug: string, + moduleId: string, + issue: IIssue, + payload: IBlockUpdateData + ) => void; + deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise; + removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, bridgeId: string) => Promise; +} + +export class ModuleIssueStore implements IModuleIssueStore { + loader: boolean = false; + error: any | null = null; + issues: { + [module_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + } = {}; + + // services + rootStore; + moduleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + issues: observable.ref, + // computed + getIssueType: computed, + getIssues: computed, + getIssuesCount: computed, + // actions + fetchIssues: action, + updateIssueStructure: action, + updateGanttIssueStructure: action, + deleteIssue: action, + addIssueToModule: action, + removeIssueFromModule: action, + }); + + this.rootStore = _rootStore; + this.moduleService = new ModuleService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + const moduleId = this.rootStore.module.moduleId; + + if ( + workspaceSlug && + projectId && + moduleId && + this.rootStore.moduleFilter.moduleFilters && + this.rootStore.issueFilter.userDisplayFilters + ) + this.fetchIssues(workspaceSlug, projectId, moduleId); + }); + } + + get getIssueType() { + const groupedLayouts = ["kanban", "list", "calendar"]; + const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; + + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null; + + if (!issueLayout) return null; + + const _issueState = groupedLayouts.includes(issueLayout) + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : ungroupedLayouts.includes(issueLayout) + ? "ungrouped" + : null; + + return _issueState || null; + } + + get getIssues() { + const moduleId: string | null = this.rootStore?.module?.moduleId; + const issueType = this.getIssueType; + if (!moduleId || !issueType) return null; + + return this.issues?.[moduleId]?.[issueType] || null; + } + + get getIssuesCount() { + const issueType = this.getIssueType; + + let issuesCount = 0; + + if (issueType === "grouped") { + const issues = this.getIssues as IIssueGroupedStructure; + + if (!issues) return 0; + + Object.keys(issues).map((group_id) => { + issuesCount += issues[group_id].length; + }); + } + + if (issueType === "groupWithSubGroups") { + const issues = this.getIssues as IIssueGroupWithSubGroupsStructure; + + if (!issues) return 0; + + Object.keys(issues).map((sub_group_id) => { + Object.keys(issues[sub_group_id]).map((group_id) => { + issuesCount += issues[sub_group_id][group_id].length; + }); + }); + } + + if (issueType === "ungrouped") { + const issues = this.getIssues as IIssueUnGroupedStructure; + + if (!issues) return 0; + + issuesCount = issues.length; + } + + return issuesCount; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const moduleId: string | null = this.rootStore?.module?.moduleId; + const issueType = this.getIssueType; + if (!moduleId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)); + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.issues = { ...this.issues, [moduleId]: { ...this.issues[moduleId], [issueType]: issues } }; + }); + }; + + updateGanttIssueStructure = async ( + workspaceSlug: string, + moduleId: string, + issue: IIssue, + payload: IBlockUpdateData + ) => { + if (!issue || !workspaceSlug) return; + + const issues = this.getIssues as IIssueUnGroupedStructure; + + const newIssues = issues.map((i) => ({ + ...i, + ...(i.id === issue.id + ? { + sort_order: payload.sort_order?.newSortOrder ?? i.sort_order, + start_date: payload.start_date, + target_date: payload.target_date, + } + : {}), + })); + + if (payload.sort_order) { + const removedElement = newIssues.splice(payload.sort_order.sourceIndex, 1)[0]; + removedElement.sort_order = payload.sort_order.newSortOrder; + newIssues.splice(payload.sort_order.destinationIndex, 0, removedElement); + } + + runInAction(() => { + this.issues = { + ...this.issues, + [moduleId]: { + ...this.issues[moduleId], + ungrouped: newIssues, + }, + }; + }); + + const newPayload: any = { ...payload }; + + if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; + + this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload); + }; + + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const moduleId: string | null = this.rootStore.module.moduleId; + const issueType = this.getIssueType; + if (!moduleId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i) => i?.id !== issue?.id); + } + + runInAction(() => { + this.issues = { ...this.issues, [moduleId]: { ...this.issues[moduleId], [issueType]: issues } }; + }); + }; + + fetchIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + this.loader = true; + this.error = null; + + const params = this.rootStore?.moduleFilter?.appliedFilters; + const issueResponse = await this.moduleService.getModuleIssuesWithParams( + workspaceSlug, + projectId, + moduleId, + params + ); + + const issueType = this.getIssueType; + if (issueType != null) { + const _issues = { + ...this.issues, + [moduleId]: { + ...this.issues[moduleId], + [issueType]: issueResponse, + }, + }; + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + this.loader = false; + this.error = error; + return error; + } + }; + + addIssueToModule = async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => { + try { + const user = this.rootStore.user.currentUser ?? undefined; + + await this.moduleService.addIssuesToModule( + workspaceSlug, + projectId, + moduleId, + { + issues: issueIds, + }, + user + ); + + this.fetchIssues(workspaceSlug, projectId, moduleId); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + removeIssueFromModule = async (workspaceSlug: string, projectId: string, moduleId: string, bridgeId: string) => { + try { + await this.moduleService.removeIssueFromModule(workspaceSlug, projectId, moduleId, bridgeId); + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; +} diff --git a/web/store/module/module_issue_calendar_view.store.ts b/web/store/module/module_issue_calendar_view.store.ts new file mode 100644 index 00000000000..3bfed3140a9 --- /dev/null +++ b/web/store/module/module_issue_calendar_view.store.ts @@ -0,0 +1,89 @@ +import { action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueType } from "store/issue"; + +export interface IModuleIssueCalendarViewStore { + // actions + handleDragDrop: (source: any, destination: any) => void; +} + +export class ModuleIssueCalendarViewStore implements IModuleIssueCalendarViewStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // actions + handleDragDrop: action, + }); + + this.rootStore = _rootStore; + } + + handleDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const moduleId = this.rootStore?.module?.moduleId; + const issueType: IIssueType | null = this.rootStore?.moduleIssue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.moduleIssue.getIssues; + + if (workspaceSlug && projectId && moduleId && issueType && issueLayout === "calendar" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; + + if (droppableSourceColumnId === droppableDestinationColumnId) return; + + if (droppableSourceColumnId != droppableDestinationColumnId) { + // horizontal + const _sourceIssues = currentIssues[droppableSourceColumnId]; + let _destinationIssues = currentIssues[droppableDestinationColumnId] || []; + + const [removed] = _sourceIssues.splice(source.index, 1); + + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + target_date: droppableDestinationColumnId, + }); + else _destinationIssues = [..._destinationIssues, { ...removed, target_date: droppableDestinationColumnId }]; + + updateIssue = { ...updateIssue, issueId: removed?.id, target_date: droppableDestinationColumnId }; + + currentIssues[droppableSourceColumnId] = _sourceIssues; + currentIssues[droppableDestinationColumnId] = _destinationIssues; + } + + const reorderedIssues = { + ...this.rootStore?.moduleIssue.issues, + [moduleId]: { + ...this.rootStore?.moduleIssue.issues?.[moduleId], + [issueType]: { + ...this.rootStore?.moduleIssue.issues?.[moduleId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.moduleIssue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + + return; + }; +} diff --git a/web/store/module/module_issue_kanban_view.store.ts b/web/store/module/module_issue_kanban_view.store.ts new file mode 100644 index 00000000000..82e210f29ff --- /dev/null +++ b/web/store/module/module_issue_kanban_view.store.ts @@ -0,0 +1,448 @@ +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueType } from "../issue/issue.store"; + +export interface IModuleIssueKanBanViewStore { + kanBanToggle: { + groupByHeaderMinMax: string[]; + subgroupByIssuesVisibility: string[]; + }; + // computed + canUserDragDrop: boolean; + canUserDragDropVertically: boolean; + canUserDragDropHorizontally: boolean; + // actions + handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void; + handleSwimlaneDragDrop: (source: any, destination: any) => void; + handleDragDrop: (source: any, destination: any) => void; +} + +export class ModuleIssueKanBanViewStore implements IModuleIssueKanBanViewStore { + kanBanToggle: { + groupByHeaderMinMax: string[]; + subgroupByIssuesVisibility: string[]; + } = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] }; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + kanBanToggle: observable, + // computed + canUserDragDrop: computed, + canUserDragDropVertically: computed, + canUserDragDropHorizontally: computed, + + // actions + handleKanBanToggle: action, + handleSwimlaneDragDrop: action, + handleDragDrop: action, + }); + + this.rootStore = _rootStore; + } + + get canUserDragDrop() { + if (this.rootStore.issueDetail.peekId) return false; + if ( + this.rootStore?.issueFilter?.userDisplayFilters?.order_by && + this.rootStore?.issueFilter?.userDisplayFilters?.order_by === "sort_order" && + this.rootStore?.issueFilter?.userDisplayFilters?.group_by && + ["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.group_by) + ) { + if (!this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by) return true; + if ( + this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by && + ["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by) + ) + return true; + } + return false; + } + + get canUserDragDropVertically() { + return false; + } + + get canUserDragDropHorizontally() { + return false; + } + + handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { + this.kanBanToggle = { + ...this.kanBanToggle, + [toggle]: this.kanBanToggle[toggle].includes(value) + ? this.kanBanToggle[toggle].filter((v) => v !== value) + : [...this.kanBanToggle[toggle], value], + }; + }; + + handleSwimlaneDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.moduleIssue.getIssues; + + const sortOrderDefaultValue = 65535; + + if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + // source, destination group and sub group id + let droppableSourceColumnId = source?.droppableId || null; + droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; + let droppableDestinationColumnId = destination?.droppableId || null; + droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; + if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; + + const source_group_id: string = droppableSourceColumnId[0]; + const source_sub_group_id: string = droppableSourceColumnId[1] === "null" ? null : droppableSourceColumnId[1]; + + const destination_group_id: string = droppableDestinationColumnId[0]; + const destination_sub_group_id: string = + droppableDestinationColumnId[1] === "null" ? null : droppableDestinationColumnId[1]; + + if (source_sub_group_id === destination_sub_group_id) { + if (source_group_id === destination_group_id) { + const _issues = currentIssues[source_sub_group_id][source_group_id]; + + // update the sort order + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _issues.length - 1) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2, + }; + } + + const [removed] = _issues.splice(source.index, 1); + _issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order }); + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_sub_group_id][source_group_id] = _issues; + } + + if (source_group_id != destination_group_id) { + const _sourceIssues = currentIssues[source_sub_group_id][source_group_id]; + let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + let issueStatePriority = {}; + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") { + updateIssue = { ...updateIssue, state: destination_group_id }; + issueStatePriority = { ...issueStatePriority, state: destination_group_id }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") { + updateIssue = { ...updateIssue, priority: destination_group_id }; + issueStatePriority = { ...issueStatePriority, priority: destination_group_id }; + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + ...issueStatePriority, + }); + else + _destinationIssues = [ + ..._destinationIssues, + { ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority }, + ]; + updateIssue = { ...updateIssue, issueId: removed?.id }; + + currentIssues[source_sub_group_id][source_group_id] = _sourceIssues; + currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues; + } + } + + if (source_sub_group_id != destination_sub_group_id) { + const _sourceIssues = currentIssues[source_sub_group_id][source_group_id]; + let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + let issueStatePriority = {}; + if (source_group_id === destination_group_id) { + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "state") { + updateIssue = { ...updateIssue, state: destination_sub_group_id }; + issueStatePriority = { ...issueStatePriority, state: destination_sub_group_id }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "priority") { + updateIssue = { ...updateIssue, priority: destination_sub_group_id }; + issueStatePriority = { ...issueStatePriority, priority: destination_sub_group_id }; + } + } else { + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "state") { + updateIssue = { ...updateIssue, state: destination_sub_group_id, priority: destination_group_id }; + issueStatePriority = { + ...issueStatePriority, + state: destination_sub_group_id, + priority: destination_group_id, + }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "priority") { + updateIssue = { ...updateIssue, state: destination_group_id, priority: destination_sub_group_id }; + issueStatePriority = { + ...issueStatePriority, + state: destination_group_id, + priority: destination_sub_group_id, + }; + } + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + ...issueStatePriority, + }); + else + _destinationIssues = [ + ..._destinationIssues, + { ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority }, + ]; + + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_sub_group_id][source_group_id] = _sourceIssues; + currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues; + } + + const reorderedIssues = { + ...this.rootStore?.moduleIssue.issues, + [projectId]: { + ...this.rootStore?.moduleIssue.issues?.[projectId], + [issueType]: { + ...this.rootStore?.moduleIssue.issues?.[projectId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.moduleIssue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + }; + + handleDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.moduleIssue.getIssues; + + const sortOrderDefaultValue = 65535; + + if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + // source, destination group and sub group id + let droppableSourceColumnId = source?.droppableId || null; + droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null; + let droppableDestinationColumnId = destination?.droppableId || null; + droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null; + if (!droppableSourceColumnId || !droppableDestinationColumnId) return null; + + const source_group_id: string = droppableSourceColumnId[0]; + const destination_group_id: string = droppableDestinationColumnId[0]; + + if (this.canUserDragDrop) { + // vertical + if (source_group_id === destination_group_id) { + const _issues = currentIssues[source_group_id]; + + // update the sort order + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _issues.length - 1) { + updateIssue = { + ...updateIssue, + sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2, + }; + } + + const [removed] = _issues.splice(source.index, 1); + _issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order }); + updateIssue = { ...updateIssue, issueId: removed?.id }; + currentIssues[source_group_id] = _issues; + } + + // horizontal + if (source_group_id != destination_group_id) { + const _sourceIssues = currentIssues[source_group_id]; + let _destinationIssues = currentIssues[destination_group_id] || []; + + if (_destinationIssues && _destinationIssues.length > 0) { + if (destination.index === 0) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue, + }; + } else if (destination.index === _destinationIssues.length) { + updateIssue = { + ...updateIssue, + sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue, + }; + } else { + updateIssue = { + ...updateIssue, + sort_order: + (_destinationIssues[destination.index - 1].sort_order + + _destinationIssues[destination.index].sort_order) / + 2, + }; + } + } else { + updateIssue = { + ...updateIssue, + sort_order: sortOrderDefaultValue, + }; + } + + let issueStatePriority = {}; + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") { + updateIssue = { ...updateIssue, state: destination_group_id }; + issueStatePriority = { ...issueStatePriority, state: destination_group_id }; + } + if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") { + updateIssue = { ...updateIssue, priority: destination_group_id }; + issueStatePriority = { ...issueStatePriority, priority: destination_group_id }; + } + + const [removed] = _sourceIssues.splice(source.index, 1); + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + sort_order: updateIssue.sort_order, + ...issueStatePriority, + }); + else + _destinationIssues = [ + ..._destinationIssues, + { ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority }, + ]; + updateIssue = { ...updateIssue, issueId: removed?.id }; + + currentIssues[source_group_id] = _sourceIssues; + currentIssues[destination_group_id] = _destinationIssues; + } + } + + // user can drag the issues only vertically + if (this.canUserDragDropVertically && destination_group_id === destination_group_id) { + } + + // user can drag the issues only horizontally + if (this.canUserDragDropHorizontally && destination_group_id != destination_group_id) { + } + + const reorderedIssues = { + ...this.rootStore?.moduleIssue.issues, + [projectId]: { + ...this.rootStore?.moduleIssue.issues?.[projectId], + [issueType]: { + ...this.rootStore?.moduleIssue.issues?.[projectId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.moduleIssue.issues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + }; +} diff --git a/web/store/module/modules.store.ts b/web/store/module/modules.store.ts new file mode 100644 index 00000000000..91a11cd76d5 --- /dev/null +++ b/web/store/module/modules.store.ts @@ -0,0 +1,398 @@ +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +// services +import { ProjectService } from "services/project"; +import { ModuleService } from "services/module.service"; +// types +import { RootStore } from "../root"; +import { IIssue, IModule } from "types"; +import { + IIssueGroupWithSubGroupsStructure, + IIssueGroupedStructure, + IIssueUnGroupedStructure, +} from "../issue/issue.store"; +import { IBlockUpdateData } from "components/gantt-chart"; + +export interface IModuleStore { + // states + loader: boolean; + error: any | null; + + // observables + moduleId: string | null; + modules: { + [project_id: string]: IModule[]; + }; + moduleDetails: { + [module_id: string]: IModule; + }; + issues: { + [module_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + + // actions + setModuleId: (moduleSlug: string) => void; + + getModuleById: (moduleId: string) => IModule | null; + + fetchModules: (workspaceSlug: string, projectId: string) => void; + fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + + createModule: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateModuleDetails: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + data: Partial + ) => Promise; + deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + updateModuleGanttStructure: ( + workspaceSlug: string, + projectId: string, + module: IModule, + payload: IBlockUpdateData + ) => void; + + // computed + projectModules: IModule[] | null; +} + +export class ModuleStore implements IModuleStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + moduleId: string | null = null; + modules: { + [project_id: string]: IModule[]; + } = {}; + moduleDetails: { + [module_id: string]: IModule; + } = {}; + issues: { + [module_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + } = {}; + + // root store + rootStore; + + // services + projectService; + moduleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable, + error: observable.ref, + + // observables + moduleId: observable.ref, + modules: observable.ref, + moduleDetails: observable.ref, + issues: observable.ref, + + // actions + setModuleId: action, + + getModuleById: action, + + fetchModules: action, + fetchModuleDetails: action, + + createModule: action, + updateModuleDetails: action, + deleteModule: action, + addModuleToFavorites: action, + removeModuleFromFavorites: action, + updateModuleGanttStructure: action, + + // computed + projectModules: computed, + }); + + this.rootStore = _rootStore; + + // services + this.projectService = new ProjectService(); + this.moduleService = new ModuleService(); + } + + // computed + get projectModules() { + if (!this.rootStore.project.projectId) return null; + + return this.modules[this.rootStore.project.projectId] || null; + } + + getModuleById = (moduleId: string) => this.moduleDetails[moduleId] || null; + + // actions + setModuleId = (moduleSlug: string) => { + this.moduleId = moduleSlug ?? null; + }; + + fetchModules = async (workspaceSlug: string, projectId: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const modulesResponse = await this.moduleService.getModules(workspaceSlug, projectId); + + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: modulesResponse, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + console.error("Failed to fetch modules list in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + } + }; + + fetchModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const response = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.moduleDetails = { + ...this.moduleDetails, + [moduleId]: response, + }; + this.loader = false; + this.error = null; + }); + + return response; + } catch (error) { + console.error("Failed to fetch module details in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + createModule = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.moduleService.createModule( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser + ); + + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: [...this.modules[projectId], response], + }; + this.loader = false; + this.error = null; + }); + + return response; + } catch (error) { + console.error("Failed to create module in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + updateModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string, data: Partial) => { + try { + runInAction(() => { + (this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => + module.id === moduleId ? { ...module, ...data } : module + ), + }), + (this.moduleDetails = { + ...this.moduleDetails, + [moduleId]: { + ...this.moduleDetails[moduleId], + ...data, + }, + }); + }); + + const user = this.rootStore.user.currentUser ?? undefined; + + const response = await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data, user); + + return response; + } catch (error) { + console.error("Failed to update module in module store", error); + + this.fetchModules(workspaceSlug, projectId); + this.fetchModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + deleteModule = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].filter((module) => module.id !== moduleId), + }; + }); + + await this.moduleService.deleteModule(workspaceSlug, projectId, moduleId, this.rootStore.user.currentUser); + } catch (error) { + console.error("Failed to delete module in module store", error); + + this.fetchModules(workspaceSlug, projectId); + + runInAction(() => { + this.error = error; + }); + } + }; + + addModuleToFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => ({ + ...module, + is_favorite: module.id === moduleId ? true : module.is_favorite, + })), + }; + }); + + await this.moduleService.addModuleToFavorites(workspaceSlug, projectId, { + module: moduleId, + }); + } catch (error) { + console.error("Failed to add module to favorites in module store", error); + + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => ({ + ...module, + is_favorite: module.id === moduleId ? false : module.is_favorite, + })), + }; + this.error = error; + }); + } + }; + + removeModuleFromFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => ({ + ...module, + is_favorite: module.id === moduleId ? false : module.is_favorite, + })), + }; + }); + + await this.moduleService.removeModuleFromFavorites(workspaceSlug, projectId, moduleId); + } catch (error) { + console.error("Failed to remove module from favorites in module store", error); + + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => ({ + ...module, + is_favorite: module.id === moduleId ? true : module.is_favorite, + })), + }; + }); + } + }; + + updateModuleGanttStructure = ( + workspaceSlug: string, + projectId: string, + module: IModule, + payload: IBlockUpdateData + ) => { + const modulesList = this.modules[projectId]; + + try { + const newModules = modulesList?.map((p: any) => ({ + ...p, + ...(p.id === module.id + ? { + start_date: payload.start_date ? payload.start_date : p.start_date, + target_date: payload.target_date ? payload.target_date : p.target_date, + sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order, + } + : {}), + })); + + if (payload.sort_order) { + const removedElement = newModules.splice(payload.sort_order.sourceIndex, 1)[0]; + newModules.splice(payload.sort_order.destinationIndex, 0, removedElement); + } + + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: newModules, + }; + }); + + const newPayload: any = { ...payload }; + + if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; + + this.updateModuleDetails(workspaceSlug, module.project, module.id, newPayload); + } catch (error) { + console.error("Failed to update module gantt structure in module store", error); + } + }; +} diff --git a/web/store/page.store.ts b/web/store/page.store.ts new file mode 100644 index 00000000000..23a8050aeea --- /dev/null +++ b/web/store/page.store.ts @@ -0,0 +1,99 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "./root"; +import { IPage } from "types"; +// services +import { ProjectService } from "services/project"; +import { PageService } from "services/page.service"; + +export interface IPageStore { + loader: boolean; + error: any | null; + + pageId: string | null; + pages: { + [project_id: string]: IPage[]; + }; + page_details: { + [page_id: string]: IPage; + }; + + //computed + projectPages: IPage[]; + // actions + setPageId: (pageId: string) => void; + fetchPages: (workspaceSlug: string, projectSlug: string) => void; +} + +class PageStore implements IPageStore { + loader: boolean = false; + error: any | null = null; + + pageId: string | null = null; + pages: { + [project_id: string]: IPage[]; + } = {}; + page_details: { + [page_id: string]: IPage; + } = {}; + + // root store + rootStore; + // service + projectService; + pageService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable, + error: observable, + + pageId: observable.ref, + pages: observable.ref, + + // computed + projectPages: computed, + // action + setPageId: action, + fetchPages: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.pageService = new PageService(); + } + + get projectPages() { + if (!this.rootStore.project.projectId) return []; + return this.pages?.[this.rootStore.project.projectId] || []; + } + + setPageId = (pageId: string) => { + this.pageId = pageId; + }; + + fetchPages = async (workspaceSlug: string, projectSlug: string) => { + try { + this.loader = true; + this.error = null; + + const pagesResponse = await this.pageService.getPagesWithParams(workspaceSlug, projectSlug, "all"); + + runInAction(() => { + this.pages = { + ...this.pages, + [projectSlug]: pagesResponse, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + console.error("Failed to fetch project pages in project store", error); + this.loader = false; + this.error = error; + } + }; +} + +export default PageStore; diff --git a/web/store/profile-issues/index.ts b/web/store/profile-issues/index.ts new file mode 100644 index 00000000000..abfd52768ec --- /dev/null +++ b/web/store/profile-issues/index.ts @@ -0,0 +1,2 @@ +export * from "./issue.store"; +export * from "./issue_filters.store"; diff --git a/web/store/profile-issues/issue.store.ts b/web/store/profile-issues/issue.store.ts new file mode 100644 index 00000000000..502fdcc523a --- /dev/null +++ b/web/store/profile-issues/issue.store.ts @@ -0,0 +1,282 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// store +import { RootStore } from "../root"; +// types +import { IIssue } from "types"; +// services +import { UserService } from "services/user.service"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; + +export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; +export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; +export type IIssueGroupWithSubGroupsStructure = { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; +}; +export type IIssueUnGroupedStructure = IIssue[]; + +export interface IProfileIssueStore { + loader: boolean; + error: any | null; + userId: string | null; + currentProfileTab: "assigned" | "created" | "subscribed" | null; + issues: { + [workspace_slug: string]: { + [user_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + }; + // computed + getIssueType: IIssueType | null; + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + // action + fetchIssues: (workspaceSlug: string, userId: string, type: "assigned" | "created" | "subscribed") => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; +} + +export class ProfileIssueStore implements IProfileIssueStore { + loader: boolean = true; + error: any | null = null; + userId: string | null = null; + currentProfileTab: "assigned" | "created" | "subscribed" | null = null; + issues: { + [workspace_slug: string]: { + [user_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + }; + } = {}; + // service + userService; + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + currentProfileTab: observable.ref, + userId: observable.ref, + issues: observable.ref, + // computed + getIssueType: computed, + getIssues: computed, + // actions + fetchIssues: action, + updateIssueStructure: action, + deleteIssue: action, + }); + this.rootStore = _rootStore; + this.userService = new UserService(); + } + + get getIssueType() { + const groupedLayouts = ["kanban", "list", "calendar"]; + const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; + + const issueLayout = this.rootStore?.profileIssueFilters?.userDisplayFilters?.layout || null; + const issueGroup = this.rootStore?.profileIssueFilters?.userDisplayFilters?.group_by || null; + const issueSubGroup = this.rootStore?.profileIssueFilters?.userDisplayFilters?.sub_group_by || null; + if (!issueLayout) return null; + + const _issueState = groupedLayouts.includes(issueLayout) + ? issueGroup + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : "ungrouped" + : ungroupedLayouts.includes(issueLayout) + ? "ungrouped" + : null; + + return _issueState || null; + } + + get getIssues() { + const workspaceSlug: string | null = this.rootStore?.workspace?.workspaceSlug; + const userId: string | null = this.userId; + const issueType = this.getIssueType; + if (!workspaceSlug || !userId || !issueType) return null; + + return this.issues?.[workspaceSlug]?.[userId]?.[issueType] || null; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const workspaceSlug: string | null = this.rootStore?.workspace?.workspaceSlug; + const userId: string | null = this.userId; + + const issueType = this.getIssueType; + if (!workspaceSlug || !userId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }; + } + + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)); + } + + const orderBy = this.rootStore?.profileIssueFilters?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.issues = { + ...this.issues, + [workspaceSlug]: { + ...this.issues?.[workspaceSlug], + [userId]: { + ...this.issues?.[workspaceSlug]?.[userId], + [issueType]: issues, + }, + }, + }; + }); + }; + + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const workspaceSlug: string | null = this.rootStore?.workspace?.workspaceSlug; + const userId: string | null = this.userId; + + const issueType = this.getIssueType; + + if (!workspaceSlug || !userId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i: IIssue) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i: IIssue) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i: IIssue) => i?.id !== issue?.id); + } + + runInAction(() => { + this.issues = { + ...this.issues, + [workspaceSlug]: { + ...this.issues?.[workspaceSlug], + [userId]: { + ...this.issues?.[workspaceSlug]?.[userId], + [issueType]: issues, + }, + }, + }; + }); + }; + + fetchIssues = async ( + workspaceSlug: string, + userId: string, + type: "assigned" | "created" | "subscribed" = "assigned" + ) => { + try { + this.loader = true; + this.error = null; + + this.currentProfileTab = type; + this.userId = userId; + + const issueType = this.getIssueType; + + let params: any = this.rootStore?.profileIssueFilters?.appliedFilters; + params = { + ...params, + assignees: undefined, + created_by: undefined, + subscriber: undefined, + }; + if (type === "assigned") params = params ? { ...params, assignees: userId } : { assignees: userId }; + else if (type === "created") params = params ? { ...params, created_by: userId } : { created_by: userId }; + else if (type === "subscribed") params = params ? { ...params, subscriber: userId } : { subscriber: userId }; + + const issueResponse = await this.userService.getUserProfileIssues(workspaceSlug, userId, params); + + if (issueType != null) { + const _issues = { + ...this.issues, + [workspaceSlug]: { + ...this.issues?.[workspaceSlug], + [userId]: { + ...this.issues?.[workspaceSlug]?.[userId], + [issueType]: issueResponse, + }, + }, + }; + + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + this.loader = false; + this.error = error; + return error; + } + }; +} diff --git a/web/store/profile-issues/issue_filters.store.ts b/web/store/profile-issues/issue_filters.store.ts new file mode 100644 index 00000000000..6573ec447d1 --- /dev/null +++ b/web/store/profile-issues/issue_filters.store.ts @@ -0,0 +1,137 @@ +import { observable, computed, makeObservable, action, autorun, runInAction } from "mobx"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; + +export interface IProfileIssueFilterStore { + userDisplayProperties: IIssueDisplayProperties; + userDisplayFilters: IIssueDisplayFilterOptions; + userFilters: IIssueFilterOptions; + // computed + appliedFilters: TIssueParams[] | null; + // action + handleIssueFilters: (type: "userFilters" | "userDisplayFilters" | "userDisplayProperties", params: any) => void; +} + +export class ProfileIssueFilterStore implements IProfileIssueFilterStore { + // observables + userFilters: IIssueFilterOptions = { + priority: null, + state_group: null, + labels: null, + start_date: null, + target_date: null, + }; + userDisplayFilters: IIssueDisplayFilterOptions = { + group_by: null, + order_by: "sort_order", + show_empty_groups: true, + type: null, + layout: "list", + }; + userDisplayProperties: any = { + assignee: true, + start_date: true, + due_date: true, + labels: true, + key: true, + priority: true, + state: true, + sub_issue_count: true, + link: true, + attachment_count: true, + estimate: true, + created_on: true, + updated_on: true, + }; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + userFilters: observable.ref, + userDisplayFilters: observable.ref, + userDisplayProperties: observable.ref, + // computed + appliedFilters: computed, + // actions + handleIssueFilters: action, + }); + + this.rootStore = _rootStore; + + autorun(() => { + if (this.userFilters || this.userDisplayFilters || this.userDisplayProperties) { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const userId = this.rootStore.profileIssues?.userId; + if (workspaceSlug && userId && this.rootStore.profileIssues.currentProfileTab && this.appliedFilters) { + console.log("autorun triggered"); + this.rootStore.profileIssues.fetchIssues( + workspaceSlug, + userId, + this.rootStore.profileIssues.currentProfileTab + ); + } + } + }); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + if (!this.userFilters || !this.userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.userFilters?.priority || undefined, + state_group: this.userFilters?.state_group || undefined, + labels: this.userFilters?.labels || undefined, + start_date: this.userFilters?.start_date || undefined, + target_date: this.userFilters?.target_date || undefined, + group_by: this.userDisplayFilters?.group_by || undefined, + order_by: this.userDisplayFilters?.order_by || "-created_at", + show_empty_groups: this.userDisplayFilters?.show_empty_groups || true, + type: this.userDisplayFilters?.type || undefined, + assignees: undefined, + created_by: undefined, + subscriber: undefined, + }; + + const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "profile_issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + return filteredRouteParams; + } + + handleIssueFilters = (type: "userFilters" | "userDisplayFilters" | "userDisplayProperties", params: any) => { + if (type === "userFilters") { + const updatedFilters = { ...this.userFilters, ...params }; + runInAction(() => { + this.userFilters = updatedFilters; + }); + } + if (type === "userDisplayFilters") { + const updatedFilters = { ...this.userDisplayFilters, ...params }; + runInAction(() => { + this.userDisplayFilters = updatedFilters; + }); + } + if (type === "userDisplayProperties") { + const updatedFilters = { ...this.userDisplayProperties, ...params }; + runInAction(() => { + this.userDisplayProperties = updatedFilters; + }); + } + }; +} diff --git a/web/store/project-view/index.ts b/web/store/project-view/index.ts new file mode 100644 index 00000000000..39a4c2facff --- /dev/null +++ b/web/store/project-view/index.ts @@ -0,0 +1,5 @@ +export * from "./project_view_filters.store"; +export * from "./project_view_issues.store"; +export * from "./project_views.store"; + +export * from "./project_view_issue_calendar_view.store"; diff --git a/web/store/project-view/project_view_filters.store.ts b/web/store/project-view/project_view_filters.store.ts new file mode 100644 index 00000000000..94d35572475 --- /dev/null +++ b/web/store/project-view/project_view_filters.store.ts @@ -0,0 +1,68 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueFilterOptions } from "types"; + +export interface IProjectViewFiltersStore { + // states + loader: boolean; + error: any | null; + + // observables + storedFilters: { + [viewId: string]: IIssueFilterOptions; + }; + + // actions + updateStoredFilters: (viewId: string, filters: Partial) => void; + deleteStoredFilters: (viewId: string) => void; +} + +export class ProjectViewFiltersStore implements IProjectViewFiltersStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + storedFilters: { + [viewId: string]: IIssueFilterOptions; + } = {}; + + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + storedFilters: observable.ref, + + // actions + updateStoredFilters: action, + deleteStoredFilters: action, + }); + + this.rootStore = _rootStore; + } + + updateStoredFilters = (viewId: string, filters: Partial) => { + runInAction(() => { + this.storedFilters = { + ...this.storedFilters, + [viewId]: { ...this.storedFilters[viewId], ...filters }, + }; + }); + }; + + deleteStoredFilters = (viewId: string) => { + const updatedStoredFilters = { ...this.storedFilters }; + delete updatedStoredFilters[viewId]; + + runInAction(() => { + this.storedFilters = updatedStoredFilters; + }); + }; +} 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 new file mode 100644 index 00000000000..9bce218aee8 --- /dev/null +++ b/web/store/project-view/project_view_issue_calendar_view.store.ts @@ -0,0 +1,89 @@ +import { action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueType } from "./project_view_issues.store"; + +export interface IProjectViewIssueCalendarViewStore { + // actions + handleDragDrop: (source: any, destination: any) => void; +} + +export class ProjectViewIssueCalendarViewStore implements IProjectViewIssueCalendarViewStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // actions + handleDragDrop: action, + }); + + this.rootStore = _rootStore; + } + + handleDragDrop = async (source: any, destination: any) => { + const workspaceSlug = this.rootStore?.workspace?.workspaceSlug; + const projectId = this.rootStore?.project?.projectId; + const viewId = this.rootStore?.projectViews?.viewId; + const issueType: IIssueType | null = this.rootStore?.projectViewIssues?.getIssueType; + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const currentIssues: any = this.rootStore.projectViewIssues.getIssues; + + if (workspaceSlug && projectId && viewId && issueType && issueLayout === "calendar" && currentIssues) { + // update issue payload + let updateIssue: any = { + workspaceSlug: workspaceSlug, + projectId: projectId, + }; + + const droppableSourceColumnId = source?.droppableId || null; + const droppableDestinationColumnId = destination?.droppableId || null; + + if (droppableSourceColumnId === droppableDestinationColumnId) return; + + if (droppableSourceColumnId != droppableDestinationColumnId) { + // horizontal + const _sourceIssues = currentIssues[droppableSourceColumnId]; + let _destinationIssues = currentIssues[droppableDestinationColumnId] || []; + + const [removed] = _sourceIssues.splice(source.index, 1); + + if (_destinationIssues && _destinationIssues.length > 0) + _destinationIssues.splice(destination.index, 0, { + ...removed, + target_date: droppableDestinationColumnId, + }); + else _destinationIssues = [..._destinationIssues, { ...removed, target_date: droppableDestinationColumnId }]; + + updateIssue = { ...updateIssue, issueId: removed?.id, target_date: droppableDestinationColumnId }; + + currentIssues[droppableSourceColumnId] = _sourceIssues; + currentIssues[droppableDestinationColumnId] = _destinationIssues; + } + + const reorderedIssues = { + ...this.rootStore?.projectViewIssues.viewIssues, + [viewId]: { + ...this.rootStore?.projectViewIssues.viewIssues?.[viewId], + [issueType]: { + ...this.rootStore?.projectViewIssues.viewIssues?.[viewId]?.[issueType], + [issueType]: currentIssues, + }, + }, + }; + + runInAction(() => { + this.rootStore.projectViewIssues.viewIssues = { ...reorderedIssues }; + }); + + this.rootStore.issueDetail?.updateIssue( + updateIssue.workspaceSlug, + updateIssue.projectId, + updateIssue.issueId, + updateIssue + ); + } + + return; + }; +} diff --git a/web/store/project-view/project_view_issues.store.ts b/web/store/project-view/project_view_issues.store.ts new file mode 100644 index 00000000000..1e699c27042 --- /dev/null +++ b/web/store/project-view/project_view_issues.store.ts @@ -0,0 +1,379 @@ +import { observable, action, makeObservable, runInAction, computed, autorun } from "mobx"; +// services +import { IssueService } from "services/issue"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; +// types +import { RootStore } from "../root"; +import { IIssue, IIssueFilterOptions } from "types"; +import { IBlockUpdateData } from "components/gantt-chart"; + +export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; +export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; +export type IIssueGroupWithSubGroupsStructure = { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; +}; +export type IIssueUnGroupedStructure = IIssue[]; + +export interface IProjectViewIssuesStore { + // states + loader: boolean; + error: any | null; + + // observables + viewIssues: { + [viewId: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + + // actions + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + updateGanttIssueStructure: (workspaceSlug: string, viewId: string, issue: IIssue, payload: IBlockUpdateData) => void; + deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + fetchViewIssues: ( + workspaceSlug: string, + projectId: string, + viewId: string, + filters: IIssueFilterOptions + ) => Promise; + + // computed + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + getIssuesCount: number; + getIssueType: IIssueType | null; +} + +export class ProjectViewIssuesStore implements IProjectViewIssuesStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + viewIssues: { + [viewId: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + } = {}; + + // root store + rootStore; + + // services + issueService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + viewIssues: observable.ref, + + // actions + updateIssueStructure: action, + updateGanttIssueStructure: action, + deleteIssue: action, + fetchViewIssues: action, + + // computed + getIssueType: computed, + getIssues: computed, + getIssuesCount: computed, + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + const viewId = this.rootStore.projectViews.viewId; + + if ( + workspaceSlug && + projectId && + viewId && + this.rootStore.projectViewFilters.storedFilters[viewId] && + this.rootStore.issueFilter.userDisplayFilters + ) + this.fetchViewIssues(workspaceSlug, projectId, viewId, this.rootStore.projectViewFilters.storedFilters[viewId]); + }); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get getIssueType() { + const groupedLayouts = ["kanban", "list", "calendar"]; + const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; + + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null; + + if (!issueLayout) return null; + + const _issueState = groupedLayouts.includes(issueLayout) + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : ungroupedLayouts.includes(issueLayout) + ? "ungrouped" + : null; + + return _issueState || null; + } + + get getIssues() { + const viewId: string | null = this.rootStore.projectViews.viewId; + const issueType = this.rootStore.issue.getIssueType; + + if (!viewId || !issueType) return null; + + return this.viewIssues?.[viewId]?.[issueType] || null; + } + + get getIssuesCount() { + const issueType = this.rootStore.issue.getIssueType; + + let issuesCount = 0; + + if (issueType === "grouped") { + const issues = this.getIssues as IIssueGroupedStructure; + + if (!issues) return 0; + + Object.keys(issues).map((group_id) => { + issuesCount += issues[group_id].length; + }); + } + + if (issueType === "groupWithSubGroups") { + const issues = this.getIssues as IIssueGroupWithSubGroupsStructure; + + if (!issues) return 0; + + Object.keys(issues).map((sub_group_id) => { + Object.keys(issues[sub_group_id]).map((group_id) => { + issuesCount += issues[sub_group_id][group_id].length; + }); + }); + } + + if (issueType === "ungrouped") { + const issues = this.getIssues as IIssueUnGroupedStructure; + + if (!issues) return 0; + + issuesCount = issues.length; + } + + return issuesCount; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const viewId: string | null = this.rootStore.projectViews.viewId; + const issueType = this.rootStore.issue.getIssueType; + if (!viewId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.map((i) => (i?.id === issue?.id ? { ...i, ...issue } : i)); + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.viewIssues = { ...this.viewIssues, [viewId]: { ...this.viewIssues[viewId], [issueType]: issues } }; + }); + }; + + updateGanttIssueStructure = async ( + workspaceSlug: string, + viewId: string, + issue: IIssue, + payload: IBlockUpdateData + ) => { + if (!issue || !workspaceSlug) return; + + const issues = this.getIssues as IIssueUnGroupedStructure; + + const newIssues = issues.map((i) => ({ + ...i, + ...(i.id === issue.id + ? { + sort_order: payload.sort_order?.newSortOrder ?? i.sort_order, + start_date: payload.start_date, + target_date: payload.target_date, + } + : {}), + })); + + if (payload.sort_order) { + const removedElement = newIssues.splice(payload.sort_order.sourceIndex, 1)[0]; + removedElement.sort_order = payload.sort_order.newSortOrder; + newIssues.splice(payload.sort_order.destinationIndex, 0, removedElement); + } + + runInAction(() => { + this.viewIssues = { + ...this.viewIssues, + [viewId]: { + ...this.viewIssues[viewId], + ungrouped: newIssues, + }, + }; + }); + + const newPayload: any = { ...payload }; + + if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder; + + this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload); + }; + + deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const viewId: string | null = this.rootStore.projectViews.viewId; + const issueType = this.rootStore.issue.getIssueType; + if (!viewId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].filter((i) => i?.id !== issue?.id), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].filter((i) => i?.id !== issue?.id), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.filter((i) => i?.id !== issue?.id); + } + + runInAction(() => { + this.viewIssues = { ...this.viewIssues, [viewId]: { ...this.viewIssues[viewId], [issueType]: issues } }; + }); + }; + + fetchViewIssues = async (workspaceSlug: string, projectId: string, viewId: string, filters: IIssueFilterOptions) => { + try { + runInAction(() => { + this.loader = true; + }); + + const displayFilters = this.rootStore.issueFilter.userDisplayFilters; + + let filteredRouteParams: any = { + priority: filters?.priority || undefined, + state_group: filters?.state_group || undefined, + state: filters?.state || undefined, + assignees: filters?.assignees || undefined, + created_by: filters?.created_by || undefined, + labels: filters?.labels || undefined, + start_date: filters?.start_date || undefined, + target_date: filters?.target_date || undefined, + group_by: displayFilters?.group_by || undefined, + order_by: displayFilters?.order_by || "-created_at", + type: displayFilters?.type || undefined, + sub_issue: displayFilters.sub_issue || undefined, + sub_group_by: displayFilters.sub_group_by || undefined, + }; + + const filteredParams = handleIssueQueryParamsByLayout(displayFilters.layout ?? "list", "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (displayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (displayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + const response = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, filteredRouteParams); + + const issueType = this.rootStore.issue.getIssueType; + + if (issueType != null) { + const newIssues = { + ...this.viewIssues, + [viewId]: { + ...this.viewIssues[viewId], + [issueType]: response, + }, + }; + + runInAction(() => { + this.loader = false; + this.viewIssues = newIssues; + }); + } + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; +} diff --git a/web/store/project-view/project_views.store.ts b/web/store/project-view/project_views.store.ts new file mode 100644 index 00000000000..4c4baf487cc --- /dev/null +++ b/web/store/project-view/project_views.store.ts @@ -0,0 +1,293 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// services +import { ViewService } from "services/view.service"; +// types +import { RootStore } from "../root"; +import { IProjectView } from "types"; + +export interface IProjectViewsStore { + // states + loader: boolean; + error: any | null; + + // observables + viewId: string | null; + viewsList: { + [projectId: string]: IProjectView[]; + }; + viewDetails: { + [viewId: string]: IProjectView; + }; + + // actions + setViewId: (viewId: string) => void; + + fetchAllViews: (workspaceSlug: string, projectId: string) => Promise; + fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + createView: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateView: ( + workspaceSlug: string, + projectId: string, + viewId: string, + data: Partial + ) => Promise; + deleteView: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise; +} + +export class ProjectViewsStore implements IProjectViewsStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + viewId: string | null = null; + viewsList: { + [projectId: string]: IProjectView[]; + } = {}; + viewDetails: { [viewId: string]: IProjectView } = {}; + + // root store + rootStore; + + // services + viewService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + viewId: observable.ref, + viewsList: observable.ref, + viewDetails: observable.ref, + + // actions + setViewId: action, + + fetchAllViews: action, + fetchViewDetails: action, + createView: action, + updateView: action, + deleteView: action, + addViewToFavorites: action, + removeViewFromFavorites: action, + }); + + this.rootStore = _rootStore; + + this.viewService = new ViewService(); + } + + setViewId = (viewId: string) => { + this.viewId = viewId; + }; + + fetchAllViews = async (workspaceSlug: string, projectId: string): Promise => { + try { + runInAction(() => { + this.loader = true; + }); + + const response = await this.viewService.getViews(workspaceSlug, projectId); + + runInAction(() => { + this.loader = false; + this.viewsList = { + ...this.viewsList, + [projectId]: response, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + fetchViewDetails = async (workspaceSlug: string, projectId: string, viewId: string): Promise => { + try { + runInAction(() => { + this.loader = true; + }); + + const response = await this.viewService.getViewDetails(workspaceSlug, projectId, viewId); + + runInAction(() => { + this.loader = false; + this.viewDetails = { + ...this.viewDetails, + [response.id]: response, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + createView = async (workspaceSlug: string, projectId: string, data: Partial): Promise => { + try { + const response = await this.viewService.createView( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser + ); + + runInAction(() => { + this.viewsList = { + ...this.viewsList, + [projectId]: [...(this.viewsList[projectId] ?? []), response], + }; + this.viewDetails = { + ...this.viewDetails, + [response.id]: response, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + updateView = async ( + workspaceSlug: string, + projectId: string, + viewId: string, + data: Partial + ): Promise => { + const viewToUpdate = { ...this.viewDetails[viewId], ...data }; + + try { + runInAction(() => { + this.viewsList = { + ...this.viewsList, + [projectId]: this.viewsList[projectId]?.map((view) => (view.id === viewId ? viewToUpdate : view)), + }; + + this.viewDetails = { + ...this.viewDetails, + [viewId]: viewToUpdate, + }; + }); + + const response = await this.viewService.patchView( + workspaceSlug, + projectId, + viewId, + data, + this.rootStore.user.currentUser + ); + + return response; + } catch (error) { + this.fetchViewDetails(workspaceSlug, projectId, viewId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + deleteView = async (workspaceSlug: string, projectId: string, viewId: string): Promise => { + try { + runInAction(() => { + this.viewsList = { + ...this.viewsList, + [projectId]: this.viewsList[projectId]?.filter((view) => view.id !== viewId), + }; + }); + + await this.viewService.deleteView(workspaceSlug, projectId, viewId, this.rootStore.user.currentUser); + } catch (error) { + this.fetchAllViews(workspaceSlug, projectId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + addViewToFavorites = async (workspaceSlug: string, projectId: string, viewId: string) => { + try { + runInAction(() => { + this.viewsList = { + ...this.viewsList, + [projectId]: this.viewsList[projectId].map((view) => ({ + ...view, + is_favorite: view.id === viewId ? true : view.is_favorite, + })), + }; + }); + + await this.viewService.addViewToFavorites(workspaceSlug, projectId, { + view: viewId, + }); + } catch (error) { + console.error("Failed to add view to favorites in view store", error); + + runInAction(() => { + this.viewsList = { + ...this.viewsList, + [projectId]: this.viewsList[projectId].map((view) => ({ + ...view, + is_favorite: view.id === viewId ? false : view.is_favorite, + })), + }; + this.error = error; + }); + } + }; + + removeViewFromFavorites = async (workspaceSlug: string, projectId: string, viewId: string) => { + try { + runInAction(() => { + this.viewsList = { + ...this.viewsList, + [projectId]: this.viewsList[projectId].map((view) => ({ + ...view, + is_favorite: view.id === viewId ? false : view.is_favorite, + })), + }; + }); + + await this.viewService.removeViewFromFavorites(workspaceSlug, projectId, viewId); + } catch (error) { + console.error("Failed to remove view from favorites in view store", error); + + runInAction(() => { + this.viewsList = { + ...this.viewsList, + [projectId]: this.viewsList[projectId].map((view) => ({ + ...view, + is_favorite: view.id === viewId ? true : view.is_favorite, + })), + }; + }); + } + }; +} diff --git a/web/store/project/index.ts b/web/store/project/index.ts new file mode 100644 index 00000000000..ccd0abfaf2e --- /dev/null +++ b/web/store/project/index.ts @@ -0,0 +1,5 @@ +export * from "./project_publish.store"; +export * from "./project.store"; +export * from "./project_estimates.store"; +export * from "./project_label_store"; +export * from "./project_state.store"; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts new file mode 100644 index 00000000000..976654ee9ee --- /dev/null +++ b/web/store/project/project.store.ts @@ -0,0 +1,695 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState, IEstimate } from "types"; +// services +import { ProjectService, ProjectStateService, ProjectEstimateService } from "services/project"; +import { IssueService, IssueLabelService } from "services/issue"; + +export interface IProjectStore { + loader: boolean; + error: any | null; + + searchQuery: string; + projectId: string | null; + projects: { [workspaceSlug: string]: IProject[] }; + 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; + projectLabels: IIssueLabels[] | null; + projectMembers: IProjectMember[] | null; + projectEstimates: IEstimate[] | null; + + joinedProjects: IProject[]; + favoriteProjects: IProject[]; + + currentProjectDetails: IProject | undefined; + + // actions + setProjectId: (projectId: string) => 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; + removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise; + + orderProjectsWithSortOrder: (sourceIndex: number, destinationIndex: number, projectId: string) => number; + updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise; + + joinProject: (workspaceSlug: string, projectIds: string[]) => Promise; + leaveProject: (workspaceSlug: string, projectId: string) => Promise; + 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 { + loader: boolean = false; + error: any | null = null; + + searchQuery: string = ""; + projectId: string | null = null; + projects: { [workspaceSlug: string]: IProject[] } = {}; // workspaceSlug: project[] + 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 = {}; + + // root store + rootStore; + // service + projectService; + issueLabelService; + issueService; + stateService; + estimateService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable, + error: observable, + + searchQuery: observable.ref, + 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, + + joinedProjects: computed, + favoriteProjects: computed, + + // action + setProjectId: action, + setSearchQuery: action, + fetchProjects: action, + fetchProjectDetails: action, + + getProjectById: action, + getProjectStateById: action, + getProjectLabelById: action, + getProjectMemberById: action, + getProjectEstimateById: action, + + fetchProjectStates: action, + fetchProjectLabels: action, + fetchProjectMembers: action, + fetchProjectEstimates: action, + + addProjectToFavorites: action, + removeProjectFromFavorites: action, + + orderProjectsWithSortOrder: action, + updateProjectView: action, + createProject: action, + updateProject: action, + leaveProject: action, + + // write operations + removeMemberFromProject: action, + updateMember: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.issueService = new IssueService(); + this.issueLabelService = new IssueLabelService(); + this.stateService = new ProjectStateService(); + this.estimateService = new ProjectEstimateService(); + } + + get searchedProjects() { + if (!this.rootStore.workspace.workspaceSlug) return []; + + const projects = this.projects[this.rootStore.workspace.workspaceSlug]; + + return this.searchQuery === "" + ? projects + : projects?.filter( + (project) => + project.name.toLowerCase().includes(this.searchQuery.toLowerCase()) || + project.identifier.toLowerCase().includes(this.searchQuery.toLowerCase()) + ); + } + + get workspaceProjects() { + if (!this.rootStore.workspace.workspaceSlug) return []; + return this.projects?.[this.rootStore.workspace.workspaceSlug]; + } + + get currentProjectDetails() { + if (!this.projectId) return; + return this.project_details[this.projectId]; + } + + get joinedProjects() { + if (!this.rootStore.workspace.workspaceSlug) return []; + return this.projects?.[this.rootStore.workspace.workspaceSlug]?.filter((p) => p.is_member); + } + + get favoriteProjects() { + if (!this.rootStore.workspace.workspaceSlug) return []; + 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; + }; + + setSearchQuery = (query: string) => { + this.searchQuery = query; + }; + + /** + * get Workspace projects using workspace slug + * @param workspaceSlug + * @returns + * + */ + fetchProjects = async (workspaceSlug: string) => { + try { + const projects = await this.projectService.getProjects(workspaceSlug); + runInAction(() => { + this.projects = { + ...this.projects, + [workspaceSlug]: projects, + }; + }); + } catch (error) { + console.log("Failed to fetch project from workspace store"); + throw error; + } + }; + + fetchProjectDetails = async (workspaceSlug: string, projectId: string) => { + try { + const response = await this.projectService.getProject(workspaceSlug, projectId); + runInAction(() => { + this.project_details = { + ...this.project_details, + [projectId]: response, + }; + }); + return response; + } catch (error) { + console.log("Error while fetching project details", error); + throw error; + } + }; + + getProjectById = (workspaceSlug: string, projectId: string) => { + const projects = this.projects?.[workspaceSlug]; + if (!projects) return null; + + const projectInfo: IProject | null = projects.find((project) => project.id === projectId) || null; + 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; + if (!labels) return null; + const labelInfo: IIssueLabels | null = labels.find((label) => label.id === labelId) || null; + 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; + if (!estimates) return null; + const estimateInfo: IEstimate | null = estimates.find((estimate) => estimate.id === estimateId) || null; + 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; + this.error = null; + + const labelResponse = await this.issueLabelService.getProjectIssueLabels(workspaceSlug, projectId); + + runInAction(() => { + this.labels = { + ...this.labels, + [projectId]: labelResponse, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + console.error(error); + this.loader = false; + this.error = error; + } + }; + + 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; + this.error = null; + + const estimatesResponse = await this.estimateService.getEstimatesList(workspaceSlug, projectId); + const _estimates = { + ...this.estimates, + [projectId]: estimatesResponse, + }; + + runInAction(() => { + this.estimates = _estimates; + this.loader = false; + this.error = null; + }); + } catch (error) { + console.error(error); + this.loader = false; + this.error = error; + } + }; + + addProjectToFavorites = async (workspaceSlug: string, projectId: string) => { + try { + runInAction(() => { + this.projects = { + ...this.projects, + [workspaceSlug]: this.projects[workspaceSlug].map((project) => { + if (project.id === projectId) { + return { ...project, is_favorite: true }; + } + return project; + }), + }; + }); + const response = await this.projectService.addProjectToFavorites(workspaceSlug, projectId); + return response; + } catch (error) { + console.log("Failed to add project to favorite"); + await this.fetchProjects(workspaceSlug); + throw error; + } + }; + + removeProjectFromFavorites = async (workspaceSlug: string, projectId: string) => { + try { + runInAction(() => { + this.projects = { + ...this.projects, + [workspaceSlug]: this.projects[workspaceSlug].map((project) => { + if (project.id === projectId) { + return { ...project, is_favorite: false }; + } + return project; + }), + }; + }); + const response = await this.projectService.removeProjectFromFavorites(workspaceSlug, projectId); + await this.fetchProjects(workspaceSlug); + return response; + } catch (error) { + console.log("Failed to add project to favorite"); + throw error; + } + }; + + orderProjectsWithSortOrder = (sortIndex: number, destinationIndex: number, projectId: string) => { + try { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + if (!workspaceSlug) return 0; + + const projectsList = this.projects[workspaceSlug] || []; + let updatedSortOrder = projectsList[sortIndex].sort_order; + + if (destinationIndex === 0) updatedSortOrder = (projectsList[0].sort_order as number) - 1000; + else if (destinationIndex === projectsList.length - 1) + updatedSortOrder = (projectsList[projectsList.length - 1].sort_order as number) + 1000; + else { + const destinationSortingOrder = projectsList[destinationIndex].sort_order as number; + const relativeDestinationSortingOrder = + sortIndex < destinationIndex + ? (projectsList[destinationIndex + 1].sort_order as number) + : (projectsList[destinationIndex - 1].sort_order as number); + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + const updatedProjectsList = projectsList.map((p) => + p.id === projectId ? { ...p, sort_order: updatedSortOrder } : p + ); + + runInAction(() => { + this.projects = { + ...this.projects, + [workspaceSlug]: updatedProjectsList, + }; + }); + + return updatedSortOrder; + } catch (error) { + console.log("failed to update sort order of the projects"); + return 0; + } + }; + + updateProjectView = async (workspaceSlug: string, projectId: string, viewProps: any) => { + try { + const response = await this.projectService.setProjectView(workspaceSlug, projectId, viewProps); + await this.fetchProjects(workspaceSlug); + + return response; + } catch (error) { + console.log("Failed to update sort order of the projects"); + throw error; + } + }; + + joinProject = async (workspaceSlug: string, projectIds: string[]) => { + const newPermissions: { [projectId: string]: boolean } = {}; + projectIds.forEach((projectId) => { + newPermissions[projectId] = true; + }); + + try { + this.loader = true; + this.error = null; + + const response = await this.projectService.joinProject(workspaceSlug, projectIds); + await this.fetchProjects(workspaceSlug); + + runInAction(() => { + this.rootStore.user.hasPermissionToProject = { + ...this.rootStore.user.hasPermissionToProject, + ...newPermissions, + }; + this.loader = false; + this.error = null; + }); + + return response; + } catch (error) { + this.loader = false; + this.error = error; + return error; + } + }; + + leaveProject = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + this.error = null; + + const response = await this.projectService.leaveProject(workspaceSlug, projectId, this.rootStore.user); + await this.fetchProjects(workspaceSlug); + + runInAction(() => { + this.loader = false; + this.error = null; + }); + + return response; + } catch (error) { + this.loader = false; + this.error = error; + return error; + } + }; + + createProject = async (workspaceSlug: string, data: any) => { + try { + const response = await this.projectService.createProject(workspaceSlug, data, this.rootStore.user.currentUser); + runInAction(() => { + this.projects = { + ...this.projects, + [workspaceSlug]: [...this.projects[workspaceSlug], response], + }; + this.project_details = { + ...this.project_details, + [response.id]: response, + }; + }); + return response; + } catch (error) { + console.log("Failed to create project from project store"); + throw error; + } + }; + + updateProject = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + runInAction(() => { + this.projects = { + ...this.projects, + [workspaceSlug]: this.projects[workspaceSlug].map((p) => (p.id === projectId ? { ...p, ...data } : p)), + }; + this.project_details = { + ...this.project_details, + [projectId]: { ...this.project_details[projectId], ...data }, + }; + }); + + const response = await this.projectService.updateProject( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser + ); + return response; + } catch (error) { + console.log("Failed to create project from project store"); + + this.fetchProjects(workspaceSlug); + this.fetchProjectDetails(workspaceSlug, projectId); + throw error; + } + }; + + deleteProject = async (workspaceSlug: string, projectId: string) => { + try { + await this.projectService.deleteProject(workspaceSlug, projectId, this.rootStore.user.currentUser); + await this.fetchProjects(workspaceSlug); + } catch (error) { + 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_estimates.store.ts b/web/store/project/project_estimates.store.ts new file mode 100644 index 00000000000..0cb51c2dca2 --- /dev/null +++ b/web/store/project/project_estimates.store.ts @@ -0,0 +1,141 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IEstimate, IEstimateFormData } from "types"; +// services +import { ProjectService, ProjectEstimateService } from "services/project"; + +export interface IProjectEstimateStore { + loader: boolean; + error: any | null; + + // estimates + createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise; + updateEstimate: ( + workspaceSlug: string, + projectId: string, + estimateId: string, + data: IEstimateFormData + ) => Promise; + deleteEstimate: (workspaceSlug: string, projectId: string, estimateId: string) => Promise; +} + +export class ProjectEstimatesStore implements IProjectEstimateStore { + loader: boolean = false; + error: any | null = null; + + // root store + rootStore; + // service + projectService; + estimateService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable, + error: observable, + + // estimates + createEstimate: action, + updateEstimate: action, + deleteEstimate: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.estimateService = new ProjectEstimateService(); + } + + createEstimate = async (workspaceSlug: string, projectId: string, data: IEstimateFormData) => { + try { + const response = await this.estimateService.createEstimate( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser! + ); + + const responseEstimate = { + ...response.estimate, + points: response.estimate_points, + }; + + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: [responseEstimate, ...(this.rootStore.project.estimates?.[projectId] || [])], + }; + }); + + return response; + } catch (error) { + console.log("Failed to create estimate from project store"); + throw error; + } + }; + + updateEstimate = async (workspaceSlug: string, projectId: string, estimateId: string, data: IEstimateFormData) => { + const originalEstimates = this.rootStore.project.getProjectEstimateById(estimateId); + + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: (this.rootStore.project.estimates?.[projectId] || [])?.map((estimate) => + estimate.id === estimateId ? { ...estimate, ...data.estimate } : estimate + ), + }; + }); + + try { + const response = await this.estimateService.patchEstimate( + workspaceSlug, + projectId, + estimateId, + data, + this.rootStore.user.currentUser! + ); + await this.rootStore.project.fetchProjectEstimates(workspaceSlug, projectId); + + return response; + } catch (error) { + console.log("Failed to update estimate from project store"); + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: (this.rootStore.project.estimates?.[projectId] || [])?.map((estimate) => + estimate.id === estimateId ? { ...estimate, ...originalEstimates } : estimate + ), + }; + }); + throw error; + } + }; + + deleteEstimate = async (workspaceSlug: string, projectId: string, estimateId: string) => { + const originalEstimateList = this.rootStore.project.projectEstimates || []; + + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: (this.rootStore.project.estimates?.[projectId] || [])?.filter( + (estimate) => estimate.id !== estimateId + ), + }; + }); + + try { + // deleting using api + await this.estimateService.deleteEstimate(workspaceSlug, projectId, estimateId, this.rootStore.user.currentUser!); + } catch (error) { + console.log("Failed to delete estimate from project store"); + // reverting back to original estimate list + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: originalEstimateList, + }; + }); + } + }; +} diff --git a/web/store/project/project_label_store.ts b/web/store/project/project_label_store.ts new file mode 100644 index 00000000000..f4ea6892ad4 --- /dev/null +++ b/web/store/project/project_label_store.ts @@ -0,0 +1,140 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueLabels } from "types"; +// services +import { IssueLabelService } from "services/issue"; +import { ProjectService } from "services/project"; + +export interface IProjectLabelStore { + loader: boolean; + error: any | null; + + // labels + createLabel: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateLabel: ( + workspaceSlug: string, + projectId: string, + labelId: string, + data: Partial + ) => Promise; + deleteLabel: (workspaceSlug: string, projectId: string, labelId: string) => Promise; +} + +export class ProjectLabelStore implements IProjectLabelStore { + loader: boolean = false; + error: any | null = null; + + // root store + rootStore; + // service + projectService; + issueLabelService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable, + error: observable, + + // labels + createLabel: action, + updateLabel: action, + deleteLabel: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.issueLabelService = new IssueLabelService(); + } + + createLabel = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.issueLabelService.createIssueLabel( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser! + ); + + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: [response, ...(this.rootStore.project.labels?.[projectId] || [])], + }; + }); + + return response; + } catch (error) { + console.log("Failed to create label from project store"); + throw error; + } + }; + + updateLabel = async (workspaceSlug: string, projectId: string, labelId: string, data: Partial) => { + const originalLabel = this.rootStore.project.getProjectLabelById(labelId); + + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: + this.rootStore.project.labels?.[projectId]?.map((label) => + label.id === labelId ? { ...label, ...data } : label + ) || [], + }; + }); + + try { + const response = await this.issueLabelService.patchIssueLabel( + workspaceSlug, + projectId, + labelId, + data, + this.rootStore.user.currentUser! + ); + + return response; + } catch (error) { + console.log("Failed to update label from project store"); + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: (this.rootStore.project.labels?.[projectId] || [])?.map((label) => + label.id === labelId ? { ...label, ...originalLabel } : label + ), + }; + }); + throw error; + } + }; + + deleteLabel = async (workspaceSlug: string, projectId: string, labelId: string) => { + const originalLabelList = this.rootStore.project.projectLabels; + + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: (this.rootStore.project.labels?.[projectId] || [])?.filter((label) => label.id !== labelId), + }; + }); + + try { + // deleting using api + await this.issueLabelService.deleteIssueLabel( + workspaceSlug, + projectId, + labelId, + this.rootStore.user.currentUser! + ); + } catch (error) { + console.log("Failed to delete label from project store"); + // reverting back to original label list + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: originalLabelList || [], + }; + }); + } + }; +} diff --git a/web/store/project/project_publish.store.ts b/web/store/project/project_publish.store.ts new file mode 100644 index 00000000000..ed427cdbe31 --- /dev/null +++ b/web/store/project/project_publish.store.ts @@ -0,0 +1,273 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +// services +import { ProjectPublishService } from "services/project"; + +export type TProjectPublishViews = "list" | "gantt" | "kanban" | "calendar" | "spreadsheet"; + +export type TProjectPublishViewsSettings = { + [key in TProjectPublishViews]: boolean; +}; + +export interface IProjectPublishSettings { + id?: string; + project?: string; + comments: boolean; + reactions: boolean; + votes: boolean; + views: TProjectPublishViewsSettings; + inbox: string | null; +} + +export interface IProjectPublishStore { + generalLoader: boolean; + fetchSettingsLoader: boolean; + error: any | null; + + projectPublishSettings: IProjectPublishSettings | "not-initialized"; + + getProjectSettingsAsync: (workspaceSlug: string, projectId: string) => Promise; + publishProject: (workspaceSlug: string, projectId: string, data: IProjectPublishSettings) => Promise; + updateProjectSettingsAsync: ( + workspaceSlug: string, + projectId: string, + projectPublishId: string, + data: IProjectPublishSettings + ) => Promise; + unPublishProject: (workspaceSlug: string, projectId: string, projectPublishId: string) => Promise; +} + +export class ProjectPublishStore implements IProjectPublishStore { + // states + generalLoader: boolean = false; + fetchSettingsLoader: boolean = false; + error: any | null = null; + + // actions + project_id: string | null = null; + projectPublishSettings: IProjectPublishSettings | "not-initialized" = "not-initialized"; + + // root store + rootStore; + + // services + projectPublishService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + generalLoader: observable, + fetchSettingsLoader: observable, + error: observable, + + // observables + project_id: observable, + projectPublishSettings: observable.ref, + + // actions + getProjectSettingsAsync: action, + publishProject: action, + updateProjectSettingsAsync: action, + unPublishProject: action, + }); + + this.rootStore = _rootStore; + + // services + this.projectPublishService = new ProjectPublishService(); + } + + getProjectSettingsAsync = async (workspaceSlug: string, projectId: string) => { + try { + runInAction(() => { + this.fetchSettingsLoader = true; + this.error = null; + }); + + const response = await this.projectPublishService.getProjectSettingsAsync(workspaceSlug, projectId); + + if (response && response.length > 0) { + const _projectPublishSettings: IProjectPublishSettings = { + id: response[0]?.id, + comments: response[0]?.comments, + reactions: response[0]?.reactions, + votes: response[0]?.votes, + views: { + list: response[0]?.views?.list || false, + kanban: response[0]?.views?.kanban || false, + calendar: response[0]?.views?.calendar || false, + gantt: response[0]?.views?.gantt || false, + spreadsheet: response[0]?.views?.spreadsheet || false, + }, + inbox: response[0]?.inbox || null, + project: response[0]?.project || null, + }; + + runInAction(() => { + this.projectPublishSettings = _projectPublishSettings; + this.fetchSettingsLoader = false; + this.error = null; + }); + } else { + runInAction(() => { + this.projectPublishSettings = "not-initialized"; + this.fetchSettingsLoader = false; + this.error = null; + }); + } + return response; + } catch (error) { + runInAction(() => { + this.fetchSettingsLoader = false; + this.error = error; + }); + + return error; + } + }; + + publishProject = async (workspaceSlug: string, projectId: string, data: IProjectPublishSettings) => { + try { + runInAction(() => { + this.generalLoader = true; + this.error = null; + }); + + const response = await this.projectPublishService.createProjectSettingsAsync(workspaceSlug, projectId, data); + + if (response) { + const _projectPublishSettings: IProjectPublishSettings = { + id: response?.id || null, + comments: response?.comments || false, + reactions: response?.reactions || false, + votes: response?.votes || false, + views: { ...response?.views }, + inbox: response?.inbox || null, + project: response?.project || null, + }; + + runInAction(() => { + this.projectPublishSettings = _projectPublishSettings; + this.rootStore.project.projects = { + ...this.rootStore.project.projects, + [workspaceSlug]: this.rootStore.project.projects[workspaceSlug].map((p) => ({ + ...p, + is_deployed: p.id === projectId ? true : p.is_deployed, + })), + }; + this.rootStore.project.project_details = { + ...this.rootStore.project.project_details, + [projectId]: { + ...this.rootStore.project.project_details[projectId], + is_deployed: true, + }, + }; + this.generalLoader = false; + this.error = null; + }); + + return response; + } + } catch (error) { + runInAction(() => { + this.generalLoader = false; + this.error = error; + }); + + return error; + } + }; + + updateProjectSettingsAsync = async ( + workspaceSlug: string, + projectId: string, + projectPublishId: string, + data: IProjectPublishSettings + ) => { + try { + runInAction(() => { + this.generalLoader = true; + this.error = null; + }); + + const response = await this.projectPublishService.updateProjectSettingsAsync( + workspaceSlug, + projectId, + projectPublishId, + data + ); + + if (response) { + const _projectPublishSettings: IProjectPublishSettings = { + id: response?.id || null, + comments: response?.comments || false, + reactions: response?.reactions || false, + votes: response?.votes || false, + views: { ...response?.views }, + inbox: response?.inbox || null, + project: response?.project || null, + }; + + runInAction(() => { + this.projectPublishSettings = _projectPublishSettings; + this.generalLoader = false; + this.error = null; + }); + + return response; + } + } catch (error) { + runInAction(() => { + this.generalLoader = false; + this.error = error; + }); + + return error; + } + }; + + unPublishProject = async (workspaceSlug: string, projectId: string, projectPublishId: string) => { + try { + runInAction(() => { + this.generalLoader = true; + this.error = null; + }); + + const response = await this.projectPublishService.deleteProjectSettingsAsync( + workspaceSlug, + projectId, + projectPublishId + ); + + runInAction(() => { + this.projectPublishSettings = "not-initialized"; + this.rootStore.project.projects = { + ...this.rootStore.project.projects, + [workspaceSlug]: this.rootStore.project.projects[workspaceSlug].map((p) => ({ + ...p, + is_deployed: p.id === projectId ? false : p.is_deployed, + })), + }; + this.rootStore.project.project_details = { + ...this.rootStore.project.project_details, + [projectId]: { + ...this.rootStore.project.project_details[projectId], + is_deployed: false, + }, + }; + this.generalLoader = false; + this.error = null; + }); + + return response; + } catch (error) { + runInAction(() => { + this.generalLoader = false; + this.error = error; + }); + + return error; + } + }; +} diff --git a/web/store/project/project_state.store.ts b/web/store/project/project_state.store.ts new file mode 100644 index 00000000000..56fc0c203ca --- /dev/null +++ b/web/store/project/project_state.store.ts @@ -0,0 +1,273 @@ +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 new file mode 100644 index 00000000000..9af4db4924b --- /dev/null +++ b/web/store/root.ts @@ -0,0 +1,231 @@ +import { enableStaticRendering } from "mobx-react-lite"; +// store imports +import CommandPaletteStore, { ICommandPaletteStore } from "./command-palette.store"; +import UserStore, { IUserStore } from "store/user.store"; +import ThemeStore, { IThemeStore } from "store/theme.store"; +import { + DraftIssuesStore, + IIssueDetailStore, + IIssueFilterStore, + IIssueKanBanViewStore, + IIssueStore, + IssueDetailStore, + IssueFilterStore, + IssueKanBanViewStore, + IIssueCalendarViewStore, + IssueCalendarViewStore, + IssueStore, + IIssueQuickAddStore, + IssueQuickAddStore, +} from "store/issue"; +import { IWorkspaceFilterStore, IWorkspaceStore, WorkspaceFilterStore, WorkspaceStore } from "store/workspace"; +import { + IProjectPublishStore, + IProjectStore, + ProjectPublishStore, + ProjectStore, + IProjectStateStore, + ProjectStateStore, + IProjectLabelStore, + ProjectLabelStore, + ProjectEstimatesStore, + IProjectEstimateStore, +} from "store/project"; +import { + IModuleFilterStore, + IModuleIssueKanBanViewStore, + IModuleIssueStore, + IModuleStore, + ModuleFilterStore, + ModuleIssueKanBanViewStore, + ModuleIssueStore, + IModuleIssueCalendarViewStore, + ModuleIssueCalendarViewStore, + ModuleStore, +} from "store/module"; +import { + CycleIssueFilterStore, + CycleIssueKanBanViewStore, + CycleIssueStore, + CycleStore, + ICycleIssueFilterStore, + ICycleIssueKanBanViewStore, + ICycleIssueCalendarViewStore, + CycleIssueCalendarViewStore, + ICycleIssueStore, + ICycleStore, +} from "store/cycle"; +import { + IProjectViewFiltersStore, + IProjectViewIssuesStore, + IProjectViewsStore, + ProjectViewFiltersStore, + ProjectViewIssuesStore, + ProjectViewsStore, + IProjectViewIssueCalendarViewStore, + ProjectViewIssueCalendarViewStore, +} from "store/project-view"; +import CalendarStore, { ICalendarStore } from "store/calendar.store"; +import { + GlobalViewFiltersStore, + GlobalViewIssuesStore, + GlobalViewsStore, + IGlobalViewFiltersStore, + IGlobalViewIssuesStore, + IGlobalViewsStore, +} from "store/global-view"; +import { + ProfileIssueStore, + IProfileIssueStore, + ProfileIssueFilterStore, + IProfileIssueFilterStore, +} from "store/profile-issues"; +import { + ArchivedIssueStore, + IArchivedIssueStore, + ArchivedIssueFilterStore, + IArchivedIssueFilterStore, + ArchivedIssueDetailStore, + IArchivedIssueDetailStore, +} from "store/archived-issues"; +import { DraftIssueStore, IDraftIssueStore, DraftIssueFilterStore, IDraftIssueFilterStore } from "store/draft-issues"; +import { + IInboxFiltersStore, + IInboxIssueDetailsStore, + IInboxIssuesStore, + IInboxStore, + InboxFiltersStore, + InboxIssueDetailsStore, + InboxIssuesStore, + InboxStore, +} from "store/inbox"; + +import { IMentionsStore, MentionsStore } from "store/editor"; + +enableStaticRendering(typeof window === "undefined"); + +export class RootStore { + user: IUserStore; + theme: IThemeStore; + + commandPalette: ICommandPaletteStore; + workspace: IWorkspaceStore; + workspaceFilter: IWorkspaceFilterStore; + + projectPublish: IProjectPublishStore; + project: IProjectStore; + projectState: IProjectStateStore; + projectLabel: IProjectLabelStore; + projectEstimates: IProjectEstimateStore; + issue: IIssueStore; + + module: IModuleStore; + moduleIssue: IModuleIssueStore; + moduleFilter: IModuleFilterStore; + moduleIssueKanBanView: IModuleIssueKanBanViewStore; + moduleIssueCalendarView: IModuleIssueCalendarViewStore; + + cycle: ICycleStore; + cycleIssue: ICycleIssueStore; + cycleIssueFilter: ICycleIssueFilterStore; + cycleIssueKanBanView: ICycleIssueKanBanViewStore; + cycleIssueCalendarView: ICycleIssueCalendarViewStore; + + projectViews: IProjectViewsStore; + projectViewIssues: IProjectViewIssuesStore; + projectViewFilters: IProjectViewFiltersStore; + projectViewIssueCalendarView: IProjectViewIssueCalendarViewStore; + + issueFilter: IIssueFilterStore; + issueDetail: IIssueDetailStore; + issueKanBanView: IIssueKanBanViewStore; + issueCalendarView: IIssueCalendarViewStore; + draftIssuesStore: DraftIssuesStore; + quickAddIssue: IIssueQuickAddStore; + + calendar: ICalendarStore; + + globalViews: IGlobalViewsStore; + globalViewIssues: IGlobalViewIssuesStore; + globalViewFilters: IGlobalViewFiltersStore; + + profileIssues: IProfileIssueStore; + profileIssueFilters: IProfileIssueFilterStore; + + archivedIssues: IArchivedIssueStore; + archivedIssueDetail: IArchivedIssueDetailStore; + archivedIssueFilters: IArchivedIssueFilterStore; + + draftIssues: IDraftIssueStore; + draftIssueFilters: IDraftIssueFilterStore; + + inbox: IInboxStore; + inboxIssues: IInboxIssuesStore; + inboxIssueDetails: IInboxIssueDetailsStore; + inboxFilters: IInboxFiltersStore; + + mentionsStore: IMentionsStore; + + constructor() { + 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.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.module = new ModuleStore(this); + this.moduleIssue = new ModuleIssueStore(this); + this.moduleFilter = new ModuleFilterStore(this); + this.moduleIssueKanBanView = new ModuleIssueKanBanViewStore(this); + this.moduleIssueCalendarView = new ModuleIssueCalendarViewStore(this); + + this.cycle = new CycleStore(this); + this.cycleIssue = new CycleIssueStore(this); + this.cycleIssueFilter = new CycleIssueFilterStore(this); + this.cycleIssueKanBanView = new CycleIssueKanBanViewStore(this); + this.cycleIssueCalendarView = new CycleIssueCalendarViewStore(this); + + this.projectViews = new ProjectViewsStore(this); + this.projectViewIssues = new ProjectViewIssuesStore(this); + this.projectViewFilters = new ProjectViewFiltersStore(this); + this.projectViewIssueCalendarView = new ProjectViewIssueCalendarViewStore(this); + + this.issue = new IssueStore(this); + this.issueFilter = new IssueFilterStore(this); + this.issueDetail = new IssueDetailStore(this); + this.issueKanBanView = new IssueKanBanViewStore(this); + this.issueCalendarView = new IssueCalendarViewStore(this); + this.draftIssuesStore = new DraftIssuesStore(this); + this.quickAddIssue = new IssueQuickAddStore(this); + + this.calendar = new CalendarStore(this); + + this.globalViews = new GlobalViewsStore(this); + this.globalViewIssues = new GlobalViewIssuesStore(this); + this.globalViewFilters = new GlobalViewFiltersStore(this); + + this.profileIssues = new ProfileIssueStore(this); + this.profileIssueFilters = new ProfileIssueFilterStore(this); + + this.archivedIssues = new ArchivedIssueStore(this); + this.archivedIssueDetail = new ArchivedIssueDetailStore(this); + this.archivedIssueFilters = new ArchivedIssueFilterStore(this); + + this.draftIssues = new DraftIssueStore(this); + this.draftIssueFilters = new DraftIssueFilterStore(this); + + this.inbox = new InboxStore(this); + this.inboxIssues = new InboxIssuesStore(this); + this.inboxIssueDetails = new InboxIssueDetailsStore(this); + this.inboxFilters = new InboxFiltersStore(this); + + this.mentionsStore = new MentionsStore(this); + } +} diff --git a/web/store/theme.store.ts b/web/store/theme.store.ts new file mode 100644 index 00000000000..51189a75d2d --- /dev/null +++ b/web/store/theme.store.ts @@ -0,0 +1,71 @@ +// mobx +import { action, observable, makeObservable } from "mobx"; +// helper +import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; + +export interface IThemeStore { + theme: string | null; + sidebarCollapsed: boolean | undefined; + + toggleSidebar: (collapsed?: boolean) => void; + setTheme: (theme: any) => void; +} + +class ThemeStore implements IThemeStore { + sidebarCollapsed: boolean | undefined = undefined; + theme: string | null = null; + // root store + rootStore; + + constructor(_rootStore: any | null = null) { + makeObservable(this, { + // observable + sidebarCollapsed: observable.ref, + theme: observable.ref, + // action + toggleSidebar: action, + setTheme: action, + // computed + }); + + this.rootStore = _rootStore; + this.initialLoad(); + } + toggleSidebar = (collapsed?: boolean) => { + if (collapsed === undefined) { + this.sidebarCollapsed = !this.sidebarCollapsed; + } else { + this.sidebarCollapsed = collapsed; + } + localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString()); + }; + + setTheme = async (_theme: { theme: any }) => { + try { + const currentTheme: string = _theme?.theme?.theme?.toString(); + + // updating the local storage theme value + localStorage.setItem("theme", currentTheme); + // updating the mobx theme value + this.theme = currentTheme; + + // applying the theme to platform if the selected theme is custom + if (currentTheme === "custom") { + const themeSettings = this.rootStore.user.currentUserSettings || null; + applyTheme( + themeSettings?.theme?.palette !== ",,,," + ? themeSettings?.theme?.palette + : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", + themeSettings?.theme?.darkPalette + ); + } else unsetCustomCssVariables(); + } catch (error) { + console.error("setting user theme error", error); + } + }; + + // init load + initialLoad() {} +} + +export default ThemeStore; diff --git a/web/store/user.store.ts b/web/store/user.store.ts new file mode 100644 index 00000000000..80cafdf44d8 --- /dev/null +++ b/web/store/user.store.ts @@ -0,0 +1,330 @@ +// mobx +import { action, observable, runInAction, makeObservable, computed } from "mobx"; +// services +import { ProjectService } from "services/project"; +import { UserService } from "services/user.service"; +import { WorkspaceService } from "services/workspace.service"; +// interfaces +import { IUser, IUserSettings } from "types/users"; +import { IWorkspaceMemberMe, IProjectMember, TUserProjectRole, TUserWorkspaceRole } from "types"; +import { RootStore } from "./root"; + +export interface IUserStore { + loader: boolean; + + isUserLoggedIn: boolean | null; + currentUser: IUser | null; + currentUserSettings: IUserSettings | null; + + dashboardInfo: any; + + workspaceMemberInfo: { + [workspaceSlug: string]: IWorkspaceMemberMe; + }; + hasPermissionToWorkspace: { + [workspaceSlug: string]: boolean | null; + }; + + projectMemberInfo: { + [projectId: string]: IProjectMember; + }; + hasPermissionToProject: { + [projectId: string]: boolean | null; + }; + + currentProjectMemberInfo: IProjectMember | undefined; + currentWorkspaceMemberInfo: IWorkspaceMemberMe | undefined; + currentProjectRole: TUserProjectRole | undefined; + currentWorkspaceRole: TUserWorkspaceRole | undefined; + + hasPermissionToCurrentWorkspace: boolean | undefined; + hasPermissionToCurrentProject: boolean | undefined; + + fetchCurrentUser: () => Promise; + fetchCurrentUserSettings: () => Promise; + + fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise; + fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise; + fetchUserDashboardInfo: (workspaceSlug: string, month: number) => Promise; + + updateUserOnBoard: () => Promise; + updateTourCompleted: () => Promise; + updateCurrentUser: (data: Partial) => Promise; + updateCurrentUserTheme: (theme: string) => Promise; +} + +class UserStore implements IUserStore { + loader: boolean = false; + + isUserLoggedIn: boolean | null = null; + currentUser: IUser | null = null; + currentUserSettings: IUserSettings | null = null; + + dashboardInfo: any = null; + + workspaceMemberInfo: { + [workspaceSlug: string]: IWorkspaceMemberMe; + } = {}; + hasPermissionToWorkspace: { + [workspaceSlug: string]: boolean; + } = {}; + + projectMemberInfo: { + [projectId: string]: IProjectMember; + } = {}; + hasPermissionToProject: { + [projectId: string]: boolean; + } = {}; + // root store + rootStore; + // services + userService; + workspaceService; + projectService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + currentUser: observable.ref, + currentUserSettings: observable.ref, + dashboardInfo: observable.ref, + workspaceMemberInfo: observable.ref, + hasPermissionToWorkspace: observable.ref, + projectMemberInfo: observable.ref, + hasPermissionToProject: observable.ref, + // action + fetchCurrentUser: action, + fetchCurrentUserSettings: action, + fetchUserDashboardInfo: action, + fetchUserWorkspaceInfo: action, + fetchUserProjectInfo: action, + updateUserOnBoard: action, + updateTourCompleted: action, + updateCurrentUser: action, + updateCurrentUserTheme: action, + // computed + currentProjectMemberInfo: computed, + currentWorkspaceMemberInfo: computed, + currentProjectRole: computed, + currentWorkspaceRole: computed, + hasPermissionToCurrentWorkspace: computed, + hasPermissionToCurrentProject: computed, + }); + this.rootStore = _rootStore; + this.userService = new UserService(); + this.workspaceService = new WorkspaceService(); + this.projectService = new ProjectService(); + } + + get currentWorkspaceMemberInfo() { + if (!this.rootStore.workspace.workspaceSlug) return; + return this.workspaceMemberInfo[this.rootStore.workspace.workspaceSlug]; + } + + get currentWorkspaceRole() { + if (!this.rootStore.workspace.workspaceSlug) return; + return this.workspaceMemberInfo[this.rootStore.workspace.workspaceSlug]?.role; + } + + get currentProjectMemberInfo() { + if (!this.rootStore.project.projectId) return; + return this.projectMemberInfo[this.rootStore.project.projectId]; + } + + get currentProjectRole() { + if (!this.rootStore.project.projectId) return; + return this.projectMemberInfo[this.rootStore.project.projectId]?.role; + } + + get hasPermissionToCurrentWorkspace() { + if (!this.rootStore.workspace.workspaceSlug) return; + return this.hasPermissionToWorkspace[this.rootStore.workspace.workspaceSlug]; + } + + get hasPermissionToCurrentProject() { + if (!this.rootStore.project.projectId) return; + return this.hasPermissionToProject[this.rootStore.project.projectId]; + } + + fetchCurrentUser = async () => { + try { + const response = await this.userService.currentUser(); + if (response) { + runInAction(() => { + this.currentUser = response; + this.isUserLoggedIn = true; + }); + } + return response; + } catch (error) { + runInAction(() => { + this.isUserLoggedIn = false; + }); + throw error; + } + }; + + fetchCurrentUserSettings = async () => { + try { + const response = await this.userService.currentUserSettings(); + if (response) { + runInAction(() => { + this.currentUserSettings = response; + }); + } + return response; + } catch (error) { + throw error; + } + }; + + fetchUserDashboardInfo = async (workspaceSlug: string, month: number) => { + try { + const response = await this.userService.userWorkspaceDashboard(workspaceSlug, month); + runInAction(() => { + this.dashboardInfo = response; + }); + return response; + } catch (error) { + throw error; + } + }; + + fetchUserWorkspaceInfo = async (workspaceSlug: string) => { + try { + const response = await this.workspaceService.workspaceMemberMe(workspaceSlug); + + runInAction(() => { + this.workspaceMemberInfo = { + ...this.workspaceMemberInfo, + [workspaceSlug]: response, + }; + this.hasPermissionToWorkspace = { + ...this.hasPermissionToWorkspace, + [workspaceSlug]: true, + }; + }); + return response; + } catch (error) { + runInAction(() => { + this.hasPermissionToWorkspace = { + ...this.hasPermissionToWorkspace, + [workspaceSlug]: false, + }; + }); + throw error; + } + }; + + fetchUserProjectInfo = async (workspaceSlug: string, projectId: string) => { + try { + const response = await this.projectService.projectMemberMe(workspaceSlug, projectId); + + runInAction(() => { + this.projectMemberInfo = { + ...this.projectMemberInfo, + [projectId]: response, + }; + this.hasPermissionToProject = { + ...this.hasPermissionToProject, + [projectId]: true, + }; + }); + return response; + } catch (error: any) { + runInAction(() => { + this.hasPermissionToProject = { + ...this.hasPermissionToProject, + [projectId]: false, + }; + }); + + throw error; + } + }; + + updateUserOnBoard = async () => { + try { + runInAction(() => { + this.currentUser = { + ...this.currentUser, + is_onboarded: true, + } as IUser; + }); + + const user = this.currentUser ?? undefined; + + if (!user) return; + + await this.userService.updateUserOnBoard({ userRole: user.role }, user); + } catch (error) { + this.fetchCurrentUser(); + + throw error; + } + }; + + updateTourCompleted = async () => { + try { + if (this.currentUser) { + runInAction(() => { + this.currentUser = { + ...this.currentUser, + is_tour_completed: true, + } as IUser; + }); + + const response = await this.userService.updateUserTourCompleted(this.currentUser); + + return response; + } + } catch (error) { + throw error; + } + }; + + updateCurrentUser = async (data: Partial) => { + try { + runInAction(() => { + this.currentUser = { + ...this.currentUser, + ...data, + } as IUser; + }); + + const response = await this.userService.updateUser(data); + + runInAction(() => { + this.currentUser = response; + }); + return response; + } catch (error) { + this.fetchCurrentUser(); + + throw error; + } + }; + + updateCurrentUserTheme = async (theme: string) => { + try { + runInAction(() => { + this.currentUser = { + ...this.currentUser, + theme: { + ...this.currentUser?.theme, + theme, + }, + } as IUser; + }); + const response = await this.userService.updateUser({ + theme: { ...this.currentUser?.theme, theme }, + } as IUser); + return response; + } catch (error) { + throw error; + } + }; +} + +export default UserStore; diff --git a/web/store/workspace/index.ts b/web/store/workspace/index.ts new file mode 100644 index 00000000000..887bd0b0d67 --- /dev/null +++ b/web/store/workspace/index.ts @@ -0,0 +1,2 @@ +export * from "./workspace_filters.store"; +export * from "./workspace.store"; diff --git a/web/store/workspace/workspace.store.ts b/web/store/workspace/workspace.store.ts new file mode 100644 index 00000000000..6891da72c89 --- /dev/null +++ b/web/store/workspace/workspace.store.ts @@ -0,0 +1,425 @@ +import { action, computed, observable, makeObservable, runInAction } from "mobx"; +import { RootStore } from "../root"; +// types +import { IIssueLabels, IProject, IWorkspace, IWorkspaceMember } from "types"; +// services +import { WorkspaceService } from "services/workspace.service"; +import { ProjectService } from "services/project"; +import { IssueService, IssueLabelService } from "services/issue"; + +export interface IWorkspaceStore { + // states + loader: boolean; + error: any | null; + + // observables + workspaceSlug: string | null; + workspaces: IWorkspace[] | undefined; + labels: { [workspaceSlug: string]: IIssueLabels[] }; // workspaceSlug: labels[] + members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[] + + // actions + setWorkspaceSlug: (workspaceSlug: string) => void; + getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null; + 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 { + // states + loader: boolean = false; + error: any | null = null; + + // observables + workspaceSlug: string | null = null; + workspaces: IWorkspace[] | undefined = []; + projects: { [workspaceSlug: string]: IProject[] } = {}; // workspaceSlug: project[] + labels: { [workspaceSlug: string]: IIssueLabels[] } = {}; + members: { [workspaceSlug: string]: IWorkspaceMember[] } = {}; + + // services + workspaceService; + projectService; + issueService; + issueLabelService; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + workspaceSlug: observable.ref, + workspaces: observable.ref, + labels: observable.ref, + members: observable.ref, + + // actions + setWorkspaceSlug: action, + getWorkspaceBySlug: action, + 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; + this.workspaceService = new WorkspaceService(); + this.projectService = new ProjectService(); + this.issueService = new IssueService(); + this.issueLabelService = new IssueLabelService(); + } + + /** + * computed value of current workspace based on workspace id saved in the store + */ + get currentWorkspace() { + if (!this.workspaceSlug) return null; + + return this.workspaces?.find((workspace) => workspace.slug === this.workspaceSlug) || null; + } + + /** + * computed value of all the workspaces created by the current logged in user + */ + get workspacesCreateByCurrentUser() { + if (!this.workspaces) return null; + + const user = this.rootStore.user.currentUser; + + if (!user) return null; + + return this.workspaces.filter((w) => w.created_by === user?.id); + } + + /** + * computed value of workspace labels using the workspace slug from the store + */ + get workspaceLabels() { + if (!this.workspaceSlug) return []; + const _labels = this.labels?.[this.workspaceSlug]; + 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 + * @returns + */ + setWorkspaceSlug = (workspaceSlug: string) => (this.workspaceSlug = workspaceSlug); + + /** + * fetch workspace info from the array of workspaces in the store. + * @param workspaceSlug + */ + getWorkspaceBySlug = (workspaceSlug: string) => this.workspaces?.find((w) => w.slug == workspaceSlug) || null; + + /** + * get workspace label information from the workspace labels + * @param labelId + * @returns + */ + getWorkspaceLabelById = (workspaceSlug: string, labelId: string) => + this.labels?.[workspaceSlug].find((label) => label.id === labelId) || null; + + /** + * fetch user workspaces from API + */ + fetchWorkspaces = async () => { + try { + this.loader = true; + this.error = null; + + const workspaceResponse = await this.workspaceService.userWorkspaces(); + + runInAction(() => { + this.workspaces = workspaceResponse; + this.loader = false; + this.error = null; + }); + + return workspaceResponse; + } catch (error) { + console.log("Failed to fetch user workspaces in workspace store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + this.workspaces = []; + }); + + throw error; + } + }; + + /** + * fetch workspace labels using workspace slug + * @param workspaceSlug + */ + fetchWorkspaceLabels = async (workspaceSlug: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const labelsResponse = await this.issueLabelService.getWorkspaceIssueLabels(workspaceSlug); + + runInAction(() => { + this.labels = { + ...this.labels, + [workspaceSlug]: labelsResponse, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + } + }; + + /** + * 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 + */ + createWorkspace = async (data: Partial) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const user = this.rootStore.user.currentUser ?? undefined; + + const response = await this.workspaceService.createWorkspace(data, user); + + runInAction(() => { + this.loader = false; + this.error = null; + this.workspaces = [...(this.workspaces ?? []), response]; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + /** + * update workspace using the workspace slug and new workspace data + * @param workspaceSlug + * @param data + */ + updateWorkspace = async (workspaceSlug: string, data: Partial) => { + const newWorkspaces = this.workspaces?.map((w) => (w.slug === workspaceSlug ? { ...w, ...data } : w)); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const user = this.rootStore.user.currentUser ?? undefined; + + const response = await this.workspaceService.updateWorkspace(workspaceSlug, data, user); + + runInAction(() => { + this.loader = false; + this.error = null; + this.workspaces = newWorkspaces; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + /** + * delete workspace using the workspace slug + * @param workspaceSlug + */ + deleteWorkspace = async (workspaceSlug: string) => { + const newWorkspaces = this.workspaces?.filter((w) => w.slug !== workspaceSlug); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const user = this.rootStore.user.currentUser ?? undefined; + + await this.workspaceService.deleteWorkspace(workspaceSlug, user); + + runInAction(() => { + this.loader = false; + this.error = null; + this.workspaces = newWorkspaces; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = 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_filters.store.ts b/web/store/workspace/workspace_filters.store.ts new file mode 100644 index 00000000000..048ee6b07aa --- /dev/null +++ b/web/store/workspace/workspace_filters.store.ts @@ -0,0 +1,193 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// services +import { WorkspaceService } from "services/workspace.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + IWorkspaceMemberMe, + IWorkspaceViewProps, + TIssueParams, +} from "types"; + +export interface IWorkspaceFilterStore { + // states + loader: boolean; + error: any | null; + + // observables + workspaceFilters: IIssueFilterOptions; + workspaceDisplayFilters: IIssueDisplayFilterOptions; + workspaceDisplayProperties: IIssueDisplayProperties; + + // actions + fetchUserWorkspaceFilters: (workspaceSlug: string) => Promise; + updateWorkspaceFilters: (workspaceSlug: string, filterToUpdate: Partial) => Promise; + + // computed + appliedFilters: TIssueParams[] | null; +} + +export class WorkspaceFilterStore implements IWorkspaceFilterStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + workspaceFilters: IIssueFilterOptions = {}; + workspaceDisplayFilters: IIssueDisplayFilterOptions = {}; + workspaceDisplayProperties: IIssueDisplayProperties = { + assignee: true, + start_date: true, + due_date: true, + labels: true, + key: true, + priority: true, + state: true, + sub_issue_count: true, + link: true, + attachment_count: true, + estimate: true, + created_on: true, + updated_on: true, + }; + + // root store + rootStore; + + // services + workspaceService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + workspaceFilters: observable.ref, + workspaceDisplayFilters: observable.ref, + workspaceDisplayProperties: observable.ref, + + // actions + fetchUserWorkspaceFilters: action, + updateWorkspaceFilters: action, + + // computed + appliedFilters: computed, + }); + + this.rootStore = _rootStore; + + this.workspaceService = new WorkspaceService(); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + if (!this.workspaceFilters || !this.workspaceDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.workspaceFilters?.priority || undefined, + state_group: this.workspaceFilters?.state_group || undefined, + state: this.workspaceFilters?.state || undefined, + assignees: this.workspaceFilters?.assignees || undefined, + created_by: this.workspaceFilters?.created_by || undefined, + labels: this.workspaceFilters?.labels || undefined, + start_date: this.workspaceFilters?.start_date || undefined, + target_date: this.workspaceFilters?.target_date || undefined, + group_by: this.workspaceDisplayFilters?.group_by || "state", + order_by: this.workspaceDisplayFilters?.order_by || "-created_at", + sub_group_by: this.workspaceDisplayFilters?.sub_group_by || undefined, + type: this.workspaceDisplayFilters?.type || undefined, + sub_issue: this.workspaceDisplayFilters?.sub_issue || true, + show_empty_groups: this.workspaceDisplayFilters?.show_empty_groups || true, + start_target_date: this.workspaceDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(this.workspaceDisplayFilters.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (this.workspaceDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (this.workspaceDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + fetchUserWorkspaceFilters = async (workspaceSlug: string) => { + try { + const memberResponse = await this.workspaceService.workspaceMemberMe(workspaceSlug); + + runInAction(() => { + this.workspaceFilters = memberResponse?.view_props?.filters; + this.workspaceDisplayFilters = memberResponse?.view_props?.display_filters ?? {}; + this.workspaceDisplayProperties = memberResponse?.view_props?.display_properties; + }); + + return memberResponse; + } catch (error) { + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + updateWorkspaceFilters = async (workspaceSlug: string, filterToUpdate: Partial) => { + const newViewProps = { + display_filters: { + ...this.workspaceDisplayFilters, + ...filterToUpdate.display_filters, + }, + display_properties: { + ...this.workspaceDisplayProperties, + ...filterToUpdate.display_properties, + }, + filters: { + ...this.workspaceFilters, + ...filterToUpdate.filters, + }, + }; + + // set sub_group_by to null if group_by is set to null + if (newViewProps.display_filters.group_by === null) newViewProps.display_filters.sub_group_by = null; + + // set group_by to state if layout is switched to kanban and group_by is null + if (newViewProps.display_filters.layout === "kanban" && newViewProps.display_filters.group_by === null) + newViewProps.display_filters.group_by = "state"; + + try { + runInAction(() => { + this.workspaceDisplayFilters = newViewProps.display_filters; + this.workspaceDisplayProperties = newViewProps.display_properties; + this.workspaceFilters = newViewProps.filters; + }); + + this.workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: newViewProps, + }); + } catch (error) { + this.fetchUserWorkspaceFilters(workspaceSlug); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user filters in issue filter store", error); + } + }; +} diff --git a/apps/app/styles/command-pallette.css b/web/styles/command-pallette.css similarity index 100% rename from apps/app/styles/command-pallette.css rename to web/styles/command-pallette.css diff --git a/web/styles/editor.css b/web/styles/editor.css new file mode 100644 index 00000000000..85d881eeb46 --- /dev/null +++ b/web/styles/editor.css @@ -0,0 +1,231 @@ +.ProseMirror p.is-editor-empty:first-child::before { + content: attr(data-placeholder); + float: left; + color: rgb(var(--color-text-400)); + pointer-events: none; + height: 0; +} + +.ProseMirror .is-empty::before { + content: attr(data-placeholder); + float: left; + color: rgb(var(--color-text-400)); + pointer-events: none; + height: 0; +} + +/* Custom image styles */ + +.ProseMirror img { + transition: filter 0.1s ease-in-out; + + &:hover { + cursor: pointer; + filter: brightness(90%); + } + + &.ProseMirror-selectednode { + outline: 3px solid #5abbf7; + filter: brightness(90%); + } +} + +.ProseMirror-gapcursor:after { + border-top: 1px solid rgb(var(--color-text-100)) !important; +} + +/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */ + +ul[data-type="taskList"] li > label { + margin-right: 0.2rem; + user-select: none; +} + +@media screen and (max-width: 768px) { + ul[data-type="taskList"] li > label { + margin-right: 0.5rem; + } +} + +ul[data-type="taskList"] li > label input[type="checkbox"] { + -webkit-appearance: none; + appearance: none; + background-color: rgb(var(--color-background-100)); + margin: 0; + cursor: pointer; + width: 1.2rem; + height: 1.2rem; + position: relative; + border: 2px solid rgb(var(--color-text-100)); + margin-right: 0.3rem; + display: grid; + place-content: center; + + &:hover { + background-color: rgb(var(--color-background-80)); + } + + &:active { + background-color: rgb(var(--color-background-90)); + } + + &::before { + content: ""; + width: 0.65em; + height: 0.65em; + transform: scale(0); + transition: 120ms transform ease-in-out; + box-shadow: inset 1em 1em; + transform-origin: center; + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); + } + + &:checked::before { + transform: scale(1); + } +} + +ul[data-type="taskList"] li[data-checked="true"] > div > p { + color: rgb(var(--color-text-200)); + text-decoration: line-through; + text-decoration-thickness: 2px; +} + +/* Overwrite tippy-box original max-width */ + +.tippy-box { + max-width: 400px !important; +} + +.ProseMirror { + position: relative; + word-wrap: break-word; + white-space: pre-wrap; + -moz-tab-size: 4; + tab-size: 4; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + outline: none; + cursor: text; + line-height: 1.2; + font-family: inherit; + font-size: 14px; + color: inherit; + -moz-box-sizing: border-box; + box-sizing: border-box; + appearance: textfield; + -webkit-appearance: textfield; + -moz-appearance: textfield; +} + +.fadeIn { + opacity: 1; + transition: opacity 0.3s ease-in; +} + +.fadeOut { + opacity: 0; + transition: opacity 0.2s ease-out; +} + +.img-placeholder { + position: relative; + width: 35%; + + &:before { + content: ""; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 45%; + width: 20px; + height: 20px; + border-radius: 50%; + border: 3px solid rgba(var(--color-text-200)); + border-top-color: rgba(var(--color-text-800)); + animation: spinning 0.6s linear infinite; + } +} + +@keyframes spinning { + to { + transform: rotate(360deg); + } +} + +#editor-container { + table { + border-collapse: collapse; + table-layout: fixed; + margin: 0; + border: 1px solid rgb(var(--color-border-200)); + width: 100%; + + td, + th { + min-width: 1em; + border: 1px solid rgb(var(--color-border-200)); + padding: 10px 15px; + vertical-align: top; + box-sizing: border-box; + position: relative; + transition: background-color 0.3s ease; + + > * { + margin-bottom: 0; + } + } + + th { + font-weight: bold; + text-align: left; + background-color: rgb(var(--color-primary-100)); + } + + td:hover { + background-color: rgba(var(--color-primary-300), 0.1); + } + + .selectedCell:after { + z-index: 2; + position: absolute; + content: ""; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: rgba(var(--color-primary-300), 0.1); + pointer-events: none; + } + + .column-resize-handle { + position: absolute; + right: -2px; + top: 0; + bottom: -2px; + width: 2px; + background-color: rgb(var(--color-primary-400)); + pointer-events: none; + } + } +} + +.tableWrapper { + overflow-x: auto; +} + +.resize-cursor { + cursor: ew-resize; + cursor: col-resize; +} + +.ProseMirror table * p { + padding: 0px 1px; + margin: 6px 2px; +} + +.ProseMirror table * .is-empty::before { + opacity: 0; +} diff --git a/web/styles/globals.css b/web/styles/globals.css new file mode 100644 index 00000000000..cde7993a109 --- /dev/null +++ b/web/styles/globals.css @@ -0,0 +1,485 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0&display=swap"); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .text-1\.5xl { + font-size: 1.375rem; + line-height: 1.875rem; + } + + .text-2\.5xl { + font-size: 1.75rem; + line-height: 2.25rem; + } +} + +@layer base { + html { + font-family: "Inter", sans-serif; + } + + :root { + color-scheme: light !important; + + --color-primary-10: 236, 241, 255; + --color-primary-20: 217, 228, 255; + --color-primary-30: 197, 214, 255; + --color-primary-40: 178, 200, 255; + --color-primary-50: 159, 187, 255; + --color-primary-60: 140, 173, 255; + --color-primary-70: 121, 159, 255; + --color-primary-80: 101, 145, 255; + --color-primary-90: 82, 132, 255; + --color-primary-100: 63, 118, 255; + --color-primary-200: 57, 106, 230; + --color-primary-300: 50, 94, 204; + --color-primary-400: 44, 83, 179; + --color-primary-500: 38, 71, 153; + --color-primary-600: 32, 59, 128; + --color-primary-700: 25, 47, 102; + --color-primary-800: 19, 35, 76; + --color-primary-900: 13, 24, 51; + + --color-background-100: 255, 255, 255; /* primary bg */ + --color-background-90: 250, 250, 250; /* secondary bg */ + --color-background-80: 245, 245, 245; /* tertiary bg */ + + --color-text-100: 23, 23, 23; /* primary text */ + --color-text-200: 58, 58, 58; /* secondary text */ + --color-text-300: 82, 82, 82; /* tertiary text */ + --color-text-400: 163, 163, 163; /* placeholder text */ + + --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 */ + --color-border-400: 185, 185, 185; /* strong border- 2 */ + + --color-shadow-2xs: 0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06), + 0px 1px 2px 0px rgba(23, 23, 23, 0.14); + --color-shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12), + 0px 1px 8px -1px rgba(16, 24, 40, 0.1); + --color-shadow-sm: 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02), + 0px 1px 12px 0px rgba(0, 0, 0, 0.12); + --color-shadow-rg: 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08), + 0px 1px 12px 0px rgba(16, 24, 40, 0.04); + --color-shadow-md: 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), + 0px 1px 16px 0px rgba(16, 24, 40, 0.12); + --color-shadow-lg: 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12), + 0px 1px 24px 0px rgba(16, 24, 40, 0.12); + --color-shadow-xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16), + 0px 0px 52px 0px rgba(16, 24, 40, 0.16); + --color-shadow-2xl: 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12), + 0px 1px 32px 0px rgba(16, 24, 40, 0.12); + --color-shadow-3xl: 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12), + 0px 1px 48px 0px rgba(16, 24, 40, 0.12); + + --color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */ + --color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */ + --color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */ + + --color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */ + --color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */ + --color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */ + --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-shadow-2xs: var(--color-shadow-2xs); + --color-sidebar-shadow-xs: var(--color-shadow-xs); + --color-sidebar-shadow-sm: var(--color-shadow-sm); + --color-sidebar-shadow-rg: var(--color-shadow-rg); + --color-sidebar-shadow-md: var(--color-shadow-md); + --color-sidebar-shadow-lg: var(--color-shadow-lg); + --color-sidebar-shadow-xl: var(--color-shadow-xl); + --color-sidebar-shadow-2xl: var(--color-shadow-2xl); + --color-sidebar-shadow-3xl: var(--color-shadow-3xl); + } + + [data-theme="light"], + [data-theme="light-contrast"] { + color-scheme: light !important; + + --color-background-100: 255, 255, 255; /* primary bg */ + --color-background-90: 250, 250, 250; /* secondary bg */ + --color-background-80: 245, 245, 245; /* tertiary bg */ + } + + [data-theme="light"] { + --color-text-100: 23, 23, 23; /* primary text */ + --color-text-200: 58, 58, 58; /* secondary text */ + --color-text-300: 82, 82, 82; /* tertiary text */ + --color-text-400: 163, 163, 163; /* placeholder text */ + + --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 */ + --color-border-400: 185, 185, 185; /* strong border- 2 */ + } + + [data-theme="light-contrast"] { + --color-text-100: 11, 11, 11; /* primary text */ + --color-text-200: 38, 38, 38; /* secondary text */ + --color-text-300: 58, 58, 58; /* tertiary text */ + --color-text-400: 115, 115, 115; /* placeholder text */ + + --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 */ + --color-border-400: 58, 58, 58; /* strong border- 2 */ + } + + [data-theme="dark"], + [data-theme="dark-contrast"] { + color-scheme: dark !important; + + --color-background-100: 7, 7, 7; /* primary bg */ + --color-background-90: 11, 11, 11; /* secondary bg */ + --color-background-80: 23, 23, 23; /* tertiary bg */ + + --color-shadow-2xs: 0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5); + --color-shadow-xs: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5); + --color-shadow-sm: 0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5); + --color-shadow-rg: 0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5); + --color-shadow-md: 0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5); + --color-shadow-lg: 0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55); + --color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55); + --color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6); + --color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65); + } + + [data-theme="dark"] { + --color-text-100: 229, 229, 229; /* primary text */ + --color-text-200: 163, 163, 163; /* secondary text */ + --color-text-300: 115, 115, 115; /* tertiary text */ + --color-text-400: 82, 82, 82; /* placeholder text */ + + --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 */ + --color-border-400: 58, 58, 58; /* strong border- 2 */ + } + + [data-theme="dark-contrast"] { + --color-text-100: 250, 250, 250; /* primary text */ + --color-text-200: 241, 241, 241; /* secondary text */ + --color-text-300: 212, 212, 212; /* tertiary text */ + --color-text-400: 115, 115, 115; /* placeholder text */ + + --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 */ + --color-border-400: 185, 185, 185; /* strong border- 2 */ + } + + [data-theme="light"], + [data-theme="dark"], + [data-theme="light-contrast"], + [data-theme="dark-contrast"] { + --color-primary-10: 236, 241, 255; + --color-primary-20: 217, 228, 255; + --color-primary-30: 197, 214, 255; + --color-primary-40: 178, 200, 255; + --color-primary-50: 159, 187, 255; + --color-primary-60: 140, 173, 255; + --color-primary-70: 121, 159, 255; + --color-primary-80: 101, 145, 255; + --color-primary-90: 82, 132, 255; + --color-primary-100: 63, 118, 255; + --color-primary-200: 57, 106, 230; + --color-primary-300: 50, 94, 204; + --color-primary-400: 44, 83, 179; + --color-primary-500: 38, 71, 153; + --color-primary-600: 32, 59, 128; + --color-primary-700: 25, 47, 102; + --color-primary-800: 19, 35, 76; + --color-primary-900: 13, 24, 51; + + --color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */ + --color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */ + --color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */ + + --color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */ + --color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */ + --color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */ + --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-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 */ + } +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + font-variant-ligatures: none; + -webkit-font-variant-ligatures: none; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} + +body { + color: rgba(var(--color-text-100)); +} + +/* scrollbar style */ +::-webkit-scrollbar { + display: none; +} + +.horizontal-scroll-enable { + overflow-x: scroll; +} + +.horizontal-scroll-enable::-webkit-scrollbar { + display: block; + height: 7px; + width: 0; +} + +.horizontal-scroll-enable::-webkit-scrollbar-track { + height: 7px; + background-color: rgba(var(--color-background-100)); +} + +.horizontal-scroll-enable::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: rgba(var(--color-background-80)); +} + +.vertical-scroll-enable::-webkit-scrollbar { + display: block; + width: 5px; +} + +.vertical-scroll-enable::-webkit-scrollbar-track { + width: 5px; +} + +.vertical-scroll-enable::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: rgba(var(--color-background-90)); +} +/* end scrollbar style */ + +.tags-input-container { + border: 2px solid #000; + padding: 0.5em; + border-radius: 3px; + width: min(80vw, 600px); + margin-top: 1em; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5em; +} + +.tag-item { + background-color: rgb(218, 216, 216); + display: inline-block; + padding: 0.5em 0.75em; + border-radius: 20px; +} +.tag-item .close { + height: 20px; + width: 20px; + background-color: rgb(48, 48, 48); + color: #fff; + border-radius: 50%; + display: inline-flex; + justify-content: center; + align-items: center; + margin-left: 0.5em; + font-size: 18px; + cursor: pointer; +} + +.tags-input { + flex-grow: 1; + padding: 0.5em 0; + border: none; + outline: none; +} + +/* emoji icon picker */ +.conical-gradient { + background: conic-gradient( + from 180deg at 50% 50%, + #ff6b00 0deg, + #f7ae59 70.5deg, + #3f76ff 151.12deg, + #05c3ff 213deg, + #18914f 289.87deg, + #f6f172 329.25deg, + #ff6b00 360deg + ); +} + +/* progress bar */ +.progress-bar { + fill: currentColor; + color: rgba(var(--color-sidebar-background-100)); +} + +/* lineclamp */ +.lineclamp { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; +} + +/* popover2 styling */ +.bp4-popover2-transition-container { + z-index: 1 !important; +} + +::-webkit-input-placeholder, +::placeholder, +:-ms-input-placeholder { + color: rgb(var(--color-text-400)); +} + +.bp4-overlay-content { + z-index: 555 !important; +} + +.disable-scroll { + overflow: hidden !important; +} + +.vertical-lr { + -webkit-writing-mode: vertical-lr; + -ms-writing-mode: vertical-lr; +} + +div.web-view-spinner { + position: relative; + width: 54px; + height: 54px; + display: inline-block; + margin-left: 50%; + margin-right: 50%; + padding: 10px; + border-radius: 10px; +} + +div.web-view-spinner div { + width: 6%; + height: 16%; + background: rgb(var(--color-text-400)); + position: absolute; + left: 49%; + top: 43%; + opacity: 0; + border-radius: 50px; + -webkit-border-radius: 50px; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); + animation: fade 1s linear infinite; + -webkit-animation: fade 1s linear infinite; +} + +@keyframes fade { + from { + opacity: 1; + } + to { + opacity: 0.25; + } +} +@-webkit-keyframes fade { + from { + opacity: 1; + } + to { + opacity: 0.25; + } +} + +div.web-view-spinner div.bar1 { + transform: rotate(0deg) translate(0, -130%); + -webkit-transform: rotate(0deg) translate(0, -130%); + animation-delay: 0s; + -webkit-animation-delay: 0s; +} + +div.web-view-spinner div.bar2 { + transform: rotate(30deg) translate(0, -130%); + -webkit-transform: rotate(30deg) translate(0, -130%); + animation-delay: -0.9167s; + -webkit-animation-delay: -0.9167s; +} + +div.web-view-spinner div.bar3 { + transform: rotate(60deg) translate(0, -130%); + -webkit-transform: rotate(60deg) translate(0, -130%); + animation-delay: -0.833s; + -webkit-animation-delay: -0.833s; +} +div.web-view-spinner div.bar4 { + transform: rotate(90deg) translate(0, -130%); + -webkit-transform: rotate(90deg) translate(0, -130%); + animation-delay: -0.7497s; + -webkit-animation-delay: -0.7497s; +} +div.web-view-spinner div.bar5 { + transform: rotate(120deg) translate(0, -130%); + -webkit-transform: rotate(120deg) translate(0, -130%); + animation-delay: -0.667s; + -webkit-animation-delay: -0.667s; +} +div.web-view-spinner div.bar6 { + transform: rotate(150deg) translate(0, -130%); + -webkit-transform: rotate(150deg) translate(0, -130%); + animation-delay: -0.5837s; + -webkit-animation-delay: -0.5837s; +} +div.web-view-spinner div.bar7 { + transform: rotate(180deg) translate(0, -130%); + -webkit-transform: rotate(180deg) translate(0, -130%); + animation-delay: -0.5s; + -webkit-animation-delay: -0.5s; +} +div.web-view-spinner div.bar8 { + transform: rotate(210deg) translate(0, -130%); + -webkit-transform: rotate(210deg) translate(0, -130%); + animation-delay: -0.4167s; + -webkit-animation-delay: -0.4167s; +} +div.web-view-spinner div.bar9 { + transform: rotate(240deg) translate(0, -130%); + -webkit-transform: rotate(240deg) translate(0, -130%); + animation-delay: -0.333s; + -webkit-animation-delay: -0.333s; +} +div.web-view-spinner div.bar10 { + transform: rotate(270deg) translate(0, -130%); + -webkit-transform: rotate(270deg) translate(0, -130%); + animation-delay: -0.2497s; + -webkit-animation-delay: -0.2497s; +} +div.web-view-spinner div.bar11 { + transform: rotate(300deg) translate(0, -130%); + -webkit-transform: rotate(300deg) translate(0, -130%); + animation-delay: -0.167s; + -webkit-animation-delay: -0.167s; +} +div.web-view-spinner div.bar12 { + transform: rotate(330deg) translate(0, -130%); + -webkit-transform: rotate(330deg) translate(0, -130%); + animation-delay: -0.0833s; + -webkit-animation-delay: -0.0833s; +} diff --git a/apps/app/styles/nprogress.css b/web/styles/nprogress.css similarity index 100% rename from apps/app/styles/nprogress.css rename to web/styles/nprogress.css diff --git a/apps/app/styles/react-datepicker.css b/web/styles/react-datepicker.css similarity index 100% rename from apps/app/styles/react-datepicker.css rename to web/styles/react-datepicker.css diff --git a/web/styles/table.css b/web/styles/table.css new file mode 100644 index 00000000000..ad88fd10ec8 --- /dev/null +++ b/web/styles/table.css @@ -0,0 +1,194 @@ +.tableWrapper { + overflow-x: auto; + padding: 2px; + width: fit-content; + max-width: 100%; +} + +.tableWrapper table { + border-collapse: collapse; + table-layout: fixed; + margin: 0; + margin-bottom: 3rem; + border: 1px solid rgba(var(--color-border-200)); + width: 100%; +} + +.tableWrapper table td, +.tableWrapper table th { + min-width: 1em; + border: 1px solid rgba(var(--color-border-200)); + padding: 10px 15px; + vertical-align: top; + box-sizing: border-box; + position: relative; + transition: background-color 0.3s ease; + + > * { + margin-bottom: 0; + } +} + +.tableWrapper table td > *, +.tableWrapper table th > * { + margin: 0 !important; + padding: 0.25rem 0 !important; +} + +.tableWrapper table td.has-focus, +.tableWrapper table th.has-focus { + box-shadow: rgba(var(--color-primary-300), 0.1) 0px 0px 0px 2px inset !important; +} + +.tableWrapper table th { + font-weight: bold; + text-align: left; + background-color: rgba(var(--color-primary-100)); +} + +.tableWrapper table th * { + font-weight: 600; +} + +.tableWrapper table .selectedCell:after { + z-index: 2; + position: absolute; + content: ""; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: rgba(var(--color-primary-300), 0.1); + pointer-events: none; +} + +.tableWrapper table .column-resize-handle { + position: absolute; + right: -2px; + top: 0; + bottom: -2px; + width: 4px; + z-index: 99; + background-color: rgba(var(--color-primary-400)); + pointer-events: none; +} + +.tableWrapper .tableControls { + position: absolute; +} + +.tableWrapper .tableControls .columnsControl, +.tableWrapper .tableControls .rowsControl { + transition: opacity ease-in 100ms; + position: absolute; + z-index: 99; + display: flex; + justify-content: center; + align-items: center; +} + +.tableWrapper .tableControls .columnsControl { + height: 20px; + transform: translateY(-50%); +} + +.tableWrapper .tableControls .columnsControl > button { + 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; + height: 15px; +} + +.tableWrapper .tableControls .rowsControl { + width: 20px; + transform: translateX(-50%); +} + +.tableWrapper .tableControls .rowsControl > button { + 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 { + 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)); + background-color: rgba(var(--color-background-100)); + padding: 0.25rem; + display: flex; + flex-direction: column; + width: 200px; + gap: 0.25rem; +} + +.tableWrapper .tableControls .tableToolbox .toolboxItem, +.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem { + background-color: rgba(var(--color-background-100)); + display: flex; + align-items: center; + gap: 0.5rem; + border: none; + padding: 0.1rem; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.tableWrapper .tableControls .tableToolbox .toolboxItem:hover, +.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem:hover { + background-color: rgba(var(--color-background-100), 0.5); +} + +.tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer, +.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer, +.tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer, +.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer { + border: 1px solid rgba(var(--color-border-300)); + border-radius: 3px; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; +} + +.tableWrapper .tableControls .tableToolbox .toolboxItem .iconContainer svg, +.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer svg, +.tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer svg, +.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer svg { + width: 2rem; + height: 2rem; +} + +.tableToolbox { + background-color: rgba(var(--color-background-100)); +} + +.tableWrapper .tableControls .tableToolbox .toolboxItem .label, +.tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .label { + font-size: 0.85rem; + color: rgba(var(--color-text-300)); +} + +.resize-cursor .tableWrapper .tableControls .rowsControl, +.tableWrapper.controls--disabled .tableControls .rowsControl, +.resize-cursor .tableWrapper .tableControls .columnsControl, +.tableWrapper.controls--disabled .tableControls .columnsControl { + opacity: 0; + pointer-events: none; +} diff --git a/web/tailwind.config.js b/web/tailwind.config.js new file mode 100644 index 00000000000..05bc93bdcd2 --- /dev/null +++ b/web/tailwind.config.js @@ -0,0 +1,5 @@ +const sharedConfig = require("tailwind-config-custom/tailwind.config.js"); + +module.exports = { + presets: [sharedConfig], +}; diff --git a/apps/app/tsconfig.json b/web/tsconfig.json similarity index 100% rename from apps/app/tsconfig.json rename to web/tsconfig.json diff --git a/apps/app/types/ai.d.ts b/web/types/ai.d.ts similarity index 100% rename from apps/app/types/ai.d.ts rename to web/types/ai.d.ts diff --git a/web/types/analytics.d.ts b/web/types/analytics.d.ts new file mode 100644 index 00000000000..35da4b723d9 --- /dev/null +++ b/web/types/analytics.d.ts @@ -0,0 +1,116 @@ +export interface IAnalyticsResponse { + total: number; + distribution: IAnalyticsData; + extras: { + assignee_details: IAnalyticsAssigneeDetails[]; + cycle_details: IAnalyticsCycleDetails[]; + label_details: IAnalyticsLabelDetails[]; + module_details: IAnalyticsModuleDetails[]; + state_details: IAnalyticsStateDetails[]; + }; +} + +export interface IAnalyticsData { + [key: string]: { + dimension: string | null; + segment?: string; + count?: number; + estimate?: number | null; + }[]; +} + +export interface IAnalyticsAssigneeDetails { + assignees__avatar: string | null; + assignees__display_name: string | null; + assignees__first_name: string; + assignees__id: string | null; + assignees__last_name: string; +} + +export interface IAnalyticsCycleDetails { + issue_cycle__cycle__name: string | null; + issue_cycle__cycle_id: string | null; +} + +export interface IAnalyticsLabelDetails { + labels__color: string | null; + labels__id: string | null; + labels__name: string | null; +} + +export interface IAnalyticsModuleDetails { + issue_module__module__name: string | null; + issue_module__module_id: string | null; +} + +export interface IAnalyticsStateDetails { + state__color: string; + state__name: string; + state_id: string; +} + +export type TXAxisValues = + | "state_id" + | "state__group" + | "labels__id" + | "assignees__id" + | "estimate_point" + | "issue_cycle__cycle_id" + | "issue_module__module_id" + | "priority" + | "start_date" + | "target_date" + | "created_at" + | "completed_at"; + +export type TYAxisValues = "issue_count" | "estimate"; + +export interface IAnalyticsParams { + x_axis: TXAxisValues; + y_axis: TYAxisValues; + segment?: TXAxisValues | null; + project?: string[] | null; + cycle?: string | null; + module?: string | null; +} + +export interface ISaveAnalyticsFormData { + name: string; + description: string; + query_dict: IExportAnalyticsFormData; +} +export interface IExportAnalyticsFormData { + x_axis: TXAxisValues; + y_axis: TYAxisValues; + segment?: TXAxisValues | null; + project?: string[]; +} + +export interface IDefaultAnalyticsUser { + assignees__avatar: string | null; + assignees__first_name: string; + assignees__last_name: string; + assignees__display_name: string; + assignees__id: string; + count: number; +} + +export interface IDefaultAnalyticsResponse { + issue_completed_month_wise: { month: number; count: number }[]; + most_issue_closed_user: IDefaultAnalyticsUser[]; + most_issue_created_user: { + created_by__avatar: string | null; + created_by__first_name: string; + created_by__last_name: string; + created_by__display_name: string; + created_by__id: string; + count: number; + }[]; + open_estimate_sum: number; + open_issues: number; + open_issues_classified: { state_group: string; state_count: number }[]; + pending_issue_user: IDefaultAnalyticsUser[]; + total_estimate_sum: number; + total_issues: number; + total_issues_classified: { state_group: string; state_count: number }[]; +} diff --git a/web/types/app.d.ts b/web/types/app.d.ts new file mode 100644 index 00000000000..2b03f6975b9 --- /dev/null +++ b/web/types/app.d.ts @@ -0,0 +1,3 @@ +export type NextPageWithLayout

= NextPage & { + getLayout?: (page: ReactElement) => ReactNode; +}; diff --git a/apps/app/types/calendar.ts b/web/types/calendar.ts similarity index 100% rename from apps/app/types/calendar.ts rename to web/types/calendar.ts diff --git a/apps/app/types/cycles.d.ts b/web/types/cycles.d.ts similarity index 88% rename from apps/app/types/cycles.d.ts rename to web/types/cycles.d.ts index 955e822224d..e97aa21339c 100644 --- a/apps/app/types/cycles.d.ts +++ b/web/types/cycles.d.ts @@ -9,6 +9,10 @@ import type { IUserLite, } from "types"; +export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; + +export type TCycleLayout = "list" | "board" | "gantt"; + export interface ICycle { backlog_issues: number; cancelled_issues: number; @@ -82,9 +86,7 @@ export interface CycleIssueResponse { sub_issues_count: number; } -export type SelectCycleType = - | (ICycle & { actionType: "edit" | "delete" | "create-issue" }) - | undefined; +export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined; export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | null; diff --git a/apps/app/types/estimate.d.ts b/web/types/estimate.d.ts similarity index 100% rename from apps/app/types/estimate.d.ts rename to web/types/estimate.d.ts diff --git a/apps/app/types/importer/github-importer.d.ts b/web/types/importer/github-importer.d.ts similarity index 100% rename from apps/app/types/importer/github-importer.d.ts rename to web/types/importer/github-importer.d.ts diff --git a/apps/app/types/importer/index.ts b/web/types/importer/index.ts similarity index 100% rename from apps/app/types/importer/index.ts rename to web/types/importer/index.ts diff --git a/apps/app/types/importer/jira-importer.d.ts b/web/types/importer/jira-importer.d.ts similarity index 100% rename from apps/app/types/importer/jira-importer.d.ts rename to web/types/importer/jira-importer.d.ts diff --git a/web/types/inbox.d.ts b/web/types/inbox.d.ts new file mode 100644 index 00000000000..10fc37b31f1 --- /dev/null +++ b/web/types/inbox.d.ts @@ -0,0 +1,61 @@ +import { IIssue } from "./issues"; +import type { IProjectLite } from "./projects"; + +export interface IInboxIssue extends IIssue { + issue_inbox: { + duplicate_to: string | null; + id: string; + snoozed_till: Date | null; + source: string; + status: -2 | -1 | 0 | 1 | 2; + }[]; +} + +export interface IInbox { + id: string; + project_detail: IProjectLite; + pending_issue_count: number; + created_at: Date; + updated_at: Date; + name: string; + description: string; + is_default: boolean; + created_by: string; + updated_by: string; + project: string; + view_props: { filters: IInboxFilterOptions }; + workspace: string; +} + +interface StatePending { + readonly status: -2; +} +interface StatusReject { + status: -1; +} + +interface StatusSnoozed { + status: 0; + snoozed_till: Date; +} + +interface StatusAccepted { + status: 1; +} + +interface StatusDuplicate { + status: 2; + duplicate_to: string; +} + +export type TInboxStatus = StatusReject | StatusSnoozed | StatusAccepted | StatusDuplicate | StatePending; + +export interface IInboxFilterOptions { + priority?: string[] | null; + inbox_status?: number[] | null; +} + +export interface IInboxQueryParams { + priority: string | null; + inbox_status: string | null; +} diff --git a/apps/app/types/index.d.ts b/web/types/index.d.ts similarity index 93% rename from apps/app/types/index.d.ts rename to web/types/index.d.ts index 8c95929178d..b350901066f 100644 --- a/apps/app/types/index.d.ts +++ b/web/types/index.d.ts @@ -18,7 +18,8 @@ export * from "./calendar"; export * from "./notifications"; export * from "./waitlist"; export * from "./reaction"; - +export * from "./view-props"; +export * from "./workspace-views"; export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object diff --git a/apps/app/types/integration.d.ts b/web/types/integration.d.ts similarity index 100% rename from apps/app/types/integration.d.ts rename to web/types/integration.d.ts diff --git a/apps/app/types/issues.d.ts b/web/types/issues.d.ts similarity index 75% rename from apps/app/types/issues.d.ts rename to web/types/issues.d.ts index 93e1598f31c..553a12ced83 100644 --- a/apps/app/types/issues.d.ts +++ b/web/types/issues.d.ts @@ -2,15 +2,14 @@ import { KeyedMutator } from "swr"; import type { IState, IUser, - IProject, ICycle, IModule, IUserLite, IProjectLite, IWorkspaceLite, IStateLite, - TStateGroups, Properties, + IIssueDisplayFilterOptions, } from "types"; export interface IIssueCycle { @@ -66,17 +65,25 @@ export interface linkDetails { url: string; } +export type IssueRelationType = "duplicate" | "relates_to" | "blocked_by"; + +export interface IssueRelation { + id: string; + issue: string; + issue_detail: BlockeIssueDetail; + relation_type: IssueRelationType; + related_issue: string; + relation: "blocking" | null; +} + export interface IIssue { archived_at: string; assignees: string[]; assignee_details: IUser[]; - assignees_list: string[]; attachment_count: number; attachments: any[]; - blocked_issues: { blocked_issue_detail?: BlockeIssueDetail }[]; - blocker_issues: { blocker_issue_detail?: BlockeIssueDetail }[]; - blockers_list: string[]; - blocks_list: string[]; + issue_relations: IssueRelation[]; + related_issues: IssueRelation[]; bridge_id?: string | null; completed_at: Date; created_at: string; @@ -89,12 +96,14 @@ export interface IIssue { description_stripped: any; estimate_point: number | null; id: string; + // tempId is used for optimistic updates. It is not a part of the API response. + tempId?: string; issue_cycle: IIssueCycle | null; issue_link: linkDetails[]; issue_module: IIssueModule | null; labels: string[]; label_details: any[]; - labels_list: string[]; + is_draft: boolean; links_list: IIssueLink[]; link_count: number; module: string | null; @@ -102,7 +111,7 @@ export interface IIssue { name: string; parent: string | null; parent_detail: IIssueParent | null; - priority: string | null; + priority: TIssuePriorities; project: string; project_detail: IProjectLite; sequence_id: number; @@ -213,55 +222,6 @@ export interface IIssueLite { workspace__slug: string; } -export interface IIssueFilterOptions { - type: "active" | "backlog" | null; - assignees: string[] | null; - start_date: string[] | null; - target_date: string[] | null; - state: string[] | null; - state_group: TStateGroups[] | null; - subscriber: string[] | null; - labels: string[] | null; - priority: string[] | null; - created_by: string[] | null; -} - -export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart"; - -export type TIssueGroupByOptions = - | "state" - | "priority" - | "labels" - | "created_by" - | "state_detail.group" - | "project" - | "assignees" - | null; - -export type TIssueOrderByOptions = - | "-created_at" - | "-updated_at" - | "priority" - | "sort_order" - | "state__name" - | "-state__name" - | "assignees__name" - | "-assignees__name" - | "labels__name" - | "-labels__name" - | "target_date" - | "-target_date" - | "estimate__point" - | "-estimate__point" - | "start_date" - | "-start_date"; - -export interface IIssueViewOptions { - group_by: TIssueGroupByOptions; - order_by: TIssueOrderByOptions; - filters: IIssueFilterOptions; -} - export interface IIssueAttachment { asset: string; attributes: { @@ -280,17 +240,16 @@ export interface IIssueAttachment { export interface IIssueViewProps { groupedIssues: { [key: string]: IIssue[] } | undefined; - groupByProperty: TIssueGroupByOptions; + displayFilters: IIssueDisplayFilterOptions | undefined; isEmpty: boolean; - issueView: TIssueViewOptions; mutateIssues: KeyedMutator< | IIssue[] | { [key: string]: IIssue[]; } >; - orderBy: TIssueOrderByOptions; params: any; properties: Properties; - showEmptyGroups: boolean; } + +export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; diff --git a/apps/app/types/modules.d.ts b/web/types/modules.d.ts similarity index 86% rename from apps/app/types/modules.d.ts rename to web/types/modules.d.ts index 709d1d300d2..6ec86c4f5f1 100644 --- a/apps/app/types/modules.d.ts +++ b/web/types/modules.d.ts @@ -10,13 +10,7 @@ import type { linkDetails, } from "types"; -export type TModuleStatus = - | "backlog" - | "planned" - | "in-progress" - | "paused" - | "completed" - | "cancelled"; +export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled"; export interface IModule { backlog_issues: number; @@ -38,7 +32,6 @@ export interface IModule { link_module: linkDetails[]; links_list: ModuleLink[]; members: string[]; - members_list: string[]; members_detail: IUserLite[]; is_favorite: boolean; name: string; @@ -80,8 +73,6 @@ export type ModuleLink = { url: string; }; -export type SelectModuleType = - | (IModule & { actionType: "edit" | "delete" | "create-issue" }) - | undefined; +export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined; export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | undefined; diff --git a/apps/app/types/notifications.d.ts b/web/types/notifications.d.ts similarity index 100% rename from apps/app/types/notifications.d.ts rename to web/types/notifications.d.ts diff --git a/apps/app/types/pages.d.ts b/web/types/pages.d.ts similarity index 97% rename from apps/app/types/pages.d.ts rename to web/types/pages.d.ts index 6be966f7ca1..f7850d11d29 100644 --- a/apps/app/types/pages.d.ts +++ b/web/types/pages.d.ts @@ -14,7 +14,6 @@ export interface IPage { is_favorite: boolean; label_details: IIssueLabels[]; labels: string[]; - labels_list: string[]; name: string; owned_by: string; project: string; diff --git a/apps/app/types/projects.d.ts b/web/types/projects.d.ts similarity index 77% rename from apps/app/types/projects.d.ts rename to web/types/projects.d.ts index 78ef4d953d8..129b0bb3b82 100644 --- a/apps/app/types/projects.d.ts +++ b/web/types/projects.d.ts @@ -1,14 +1,6 @@ -import type { - IIssueFilterOptions, - IUserLite, - IWorkspace, - IWorkspaceLite, - IUserMemberLite, - TIssueGroupByOptions, - TIssueOrderByOptions, - TIssueViewOptions, - TStateGroups, -} from "./"; +import type { IUserLite, IWorkspace, IWorkspaceLite, IUserMemberLite, TStateGroups, IProjectViewProps } from "."; + +export type TUserProjectRole = 5 | 10 | 15 | 20; export interface IProject { archive_in: number; @@ -42,7 +34,8 @@ export interface IProject { is_deployed: boolean; is_favorite: boolean; is_member: boolean; - member_role: 5 | 10 | 15 | 20 | null; + member_role: TUserProjectRole | null; + members: IProjectMemberLite[]; issue_views_view: boolean; module_view: boolean; name: string; @@ -50,7 +43,6 @@ export interface IProject { page_view: boolean; project_lead: IUserLite | string | null; sort_order: number | null; - slug: string; total_cycles: number; total_members: number; total_modules: number; @@ -66,32 +58,31 @@ export interface IProjectLite { identifier: string; } -type ProjectViewTheme = { - issueView: TIssueViewOptions; - groupByProperty: TIssueGroupByOptions; - orderBy: TIssueOrderByOptions; - calendarDateRange: string; - filters: IIssueFilterOptions; -}; - type ProjectPreferences = { pages: { block_display: boolean; }; }; +export interface IProjectMemberLite { + id: string; + member__avatar: string; + member__display_name: string; + member_id: string; +} + export interface IProjectMember { id: string; member: IUserMemberLite; project: IProjectLite; workspace: IWorkspaceLite; comment: string; - role: 5 | 10 | 15 | 20; + role: TUserProjectRole; preferences: ProjectPreferences; - view_props: ProjectViewTheme; - default_props: ProjectViewTheme; + view_props: IProjectViewProps; + default_props: IProjectViewProps; created_at: Date; updated_at: Date; @@ -110,7 +101,7 @@ export interface IProjectMemberInvitation { token: string; message: string; responded_at: Date; - role: 5 | 10 | 15 | 20; + role: TUserProjectRole; created_at: Date; updated_at: Date; @@ -118,8 +109,8 @@ export interface IProjectMemberInvitation { updated_by: string; } -export interface IProjectBulkInviteFormData { - members: { role: 5 | 10 | 15 | 20; member_id: string }[]; +export interface IProjectBulkAddFormData { + members: { role: TUserProjectRole; member_id: string }[]; } export interface IGithubRepository { @@ -137,7 +128,7 @@ export interface GithubRepositoriesResponse { export type TProjectIssuesSearchParams = { search: string; parent?: boolean; - blocker_blocked_by?: boolean; + issue_relation?: boolean; cycle?: boolean; module?: boolean; sub_issue?: boolean; diff --git a/apps/app/types/reaction.d.ts b/web/types/reaction.d.ts similarity index 100% rename from apps/app/types/reaction.d.ts rename to web/types/reaction.d.ts diff --git a/apps/app/types/state.d.ts b/web/types/state.d.ts similarity index 100% rename from apps/app/types/state.d.ts rename to web/types/state.d.ts diff --git a/web/types/users.d.ts b/web/types/users.d.ts new file mode 100644 index 00000000000..2c93ff764c0 --- /dev/null +++ b/web/types/users.d.ts @@ -0,0 +1,186 @@ +import { IIssueActivity, IIssueLite, TStateGroups } from "."; + +export interface IUser { + id: string; + avatar: string; + cover_image: string | null; + date_joined: string; + display_name: string; + email: string; + first_name: string; + last_name: string; + is_active: boolean; + is_bot: boolean; + is_email_verified: boolean; + is_managed: boolean; + is_onboarded: boolean; + is_tour_completed: boolean; + mobile_number: string | null; + role: string | null; + onboarding_step: { + workspace_join?: boolean; + profile_complete?: boolean; + workspace_create?: boolean; + workspace_invite?: boolean; + }; + last_workspace_id: string; + user_timezone: string; + username: string; + theme: IUserTheme; +} + +export interface IUserSettings { + id: string; + email: string; + workspace: { + last_workspace_id: string; + last_workspace_slug: string; + fallback_workspace_id: string; + fallback_workspace_slug: string; + invites: number; + }; +} + +export interface IUserTheme { + background: string; + text: string; + primary: string; + sidebarBackground: string; + sidebarText: string; + darkPalette: boolean; + palette: string; + theme: string; +} + +export interface IUserLite { + avatar: string; + created_at: Date; + display_name: string; + email?: string; + first_name: string; + readonly id: string; + is_bot: boolean; + last_name: string; +} + +export interface IUserMemberLite extends IUserLite { + email?: string; +} + +export interface IUserActivity { + created_date: string; + activity_count: number; +} + +export interface IUserPriorityDistribution { + priority: string; + priority_count: number; +} + +export interface IUserStateDistribution { + state_group: TStateGroups; + state_count: number; +} + +export interface IUserWorkspaceDashboard { + assigned_issues_count: number; + completed_issues_count: number; + issue_activities: IUserActivity[]; + issues_due_week_count: number; + overdue_issues: IIssueLite[]; + completed_issues: { + week_in_month: number; + completed_count: number; + }[]; + pending_issues_count: number; + state_distribution: IUserStateDistribution[]; + upcoming_issues: IIssueLite[]; +} + +export interface IUserActivityResponse { + count: number; + extra_stats: null; + next_cursor: string; + next_page_results: boolean; + prev_cursor: string; + prev_page_results: boolean; + results: IIssueActivity[]; + total_pages: number; +} + +export type UserAuth = { + isMember: boolean; + isOwner: boolean; + isViewer: boolean; + isGuest: boolean; +}; + +export type TOnboardingSteps = { + profile_complete: boolean; + workspace_create: boolean; + workspace_invite: boolean; + workspace_join: boolean; +}; + +export interface IUserProfileData { + assigned_issues: number; + completed_issues: number; + created_issues: number; + pending_issues: number; + priority_distribution: IUserPriorityDistribution[]; + state_distribution: IUserStateDistribution[]; + subscribed_issues: number; +} + +export interface IUserProfileProjectSegregation { + project_data: { + assigned_issues: number; + completed_issues: number; + created_issues: number; + emoji: string | null; + icon_prop: null; + id: string; + identifier: string; + name: string; + pending_issues: number; + }[]; + user_data: { + avatar: string; + cover_image: string | null; + date_joined: Date; + display_name: string; + first_name: string; + last_name: string; + user_timezone: string; + }; +} + +// export interface ICurrentUser { +// id: readonly string; +// avatar: string; +// first_name: string; +// last_name: string; +// username: string; +// email: string; +// mobile_number: string; +// is_email_verified: boolean; +// is_tour_completed: boolean; +// onboarding_step: TOnboardingSteps; +// is_onboarded: boolean; +// role: string; +// } + +// export interface ICustomTheme { +// background: string; +// text: string; +// primary: string; +// sidebarBackground: string; +// sidebarText: string; +// darkPalette: boolean; +// palette: string; +// theme: string; +// } + +// export interface ICurrentUserSettings { +// theme: ICustomTheme; +// } diff --git a/web/types/view-props.d.ts b/web/types/view-props.d.ts new file mode 100644 index 00000000000..c8c47576b9a --- /dev/null +++ b/web/types/view-props.d.ts @@ -0,0 +1,160 @@ +export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart"; + +export type TIssueGroupByOptions = + | "state" + | "priority" + | "labels" + | "created_by" + | "state_detail.group" + | "project" + | "assignees" + | "mentions" + | null; + +export type TIssueOrderByOptions = + | "-created_at" + | "created_at" + | "updated_at" + | "-updated_at" + | "priority" + | "-priority" + | "sort_order" + | "state__name" + | "-state__name" + | "assignees__first_name" + | "-assignees__first_name" + | "labels__name" + | "-labels__name" + | "target_date" + | "-target_date" + | "estimate_point" + | "-estimate_point" + | "start_date" + | "-start_date" + | "link_count" + | "-link_count" + | "attachment_count" + | "-attachment_count" + | "sub_issues_count" + | "-sub_issues_count"; + +export type TIssueTypeFilters = "active" | "backlog" | null; + +export type TIssueExtraOptions = "show_empty_groups" | "sub_issue"; + +export type TIssueParams = + | "priority" + | "state_group" + | "state" + | "assignees" + | "mentions" + | "created_by" + | "subscriber" + | "labels" + | "start_date" + | "target_date" + | "project" + | "group_by" + | "sub_group_by" + | "order_by" + | "type" + | "sub_issue" + | "show_empty_groups" + | "start_target_date"; + +export type TCalendarLayouts = "month" | "week"; + +export interface IIssueFilterOptions { + assignees?: string[] | null; + mentions?: string[] | null; + created_by?: string[] | null; + labels?: string[] | null; + priority?: string[] | null; + project?: string[] | null; + start_date?: string[] | null; + state?: string[] | null; + state_group?: string[] | null; + subscriber?: string[] | null; + target_date?: string[] | null; +} + +export interface IIssueDisplayFilterOptions { + calendar?: { + show_weekends?: boolean; + layout?: TCalendarLayouts; + }; + group_by?: TIssueGroupByOptions; + sub_group_by?: TIssueGroupByOptions; + layout?: TIssueLayouts; + order_by?: TIssueOrderByOptions; + show_empty_groups?: boolean; + start_target_date?: boolean; + sub_issue?: boolean; + type?: TIssueTypeFilters; +} +export interface IIssueDisplayProperties { + assignee?: boolean; + start_date?: boolean; + due_date?: boolean; + labels?: boolean; + key?: boolean; + priority?: boolean; + state?: boolean; + sub_issue_count?: boolean; + link?: boolean; + attachment_count?: boolean; + estimate?: boolean; + created_on?: boolean; + updated_on?: boolean; +} + +export interface IWorkspaceIssueFilterOptions { + assignees?: string[] | null; + created_by?: string[] | null; + labels?: string[] | null; + priority?: string[] | null; + state_group?: string[] | null; + subscriber?: string[] | null; + start_date?: string[] | null; + target_date?: string[] | null; + project?: string[] | null; +} + +export interface IWorkspaceGlobalViewDisplayFilterOptions { + order_by?: string | undefined; + type?: "active" | "backlog" | null; + sub_issue?: boolean; + layout?: TIssueViewOptions; +} + +export interface IWorkspaceViewIssuesParams { + assignees?: string | undefined; + created_by?: string | undefined; + labels?: string | undefined; + priority?: string | undefined; + start_date?: string | undefined; + state?: string | undefined; + state_group?: string | undefined; + subscriber?: string | undefined; + target_date?: string | undefined; + project?: string | undefined; + order_by?: string | undefined; + type?: "active" | "backlog" | undefined; + sub_issue?: boolean; +} + +export interface IProjectViewProps { + display_filters: IIssueDisplayFilterOptions | undefined; + filters: IIssueFilterOptions; +} + +export interface IWorkspaceViewProps { + filters: IIssueFilterOptions; + display_filters: IIssueDisplayFilterOptions | undefined; + display_properties: IIssueDisplayProperties; +} +export interface IWorkspaceGlobalViewProps { + filters: IWorkspaceIssueFilterOptions; + display_filters: IWorkspaceIssueDisplayFilterOptions | undefined; + display_properties: IIssueDisplayProperties; +} diff --git a/web/types/views.d.ts b/web/types/views.d.ts new file mode 100644 index 00000000000..4f55e8c7459 --- /dev/null +++ b/web/types/views.d.ts @@ -0,0 +1,17 @@ +import { IIssueFilterOptions } from "./view-props"; + +export interface IProjectView { + id: string; + access: string; + created_at: Date; + updated_at: Date; + is_favorite: boolean; + created_by: string; + updated_by: string; + name: string; + description: string; + query: IIssueFilterOptions; + query_data: IIssueFilterOptions; + project: string; + workspace: string; +} diff --git a/apps/app/types/waitlist.d.ts b/web/types/waitlist.d.ts similarity index 100% rename from apps/app/types/waitlist.d.ts rename to web/types/waitlist.d.ts diff --git a/web/types/workspace-views.d.ts b/web/types/workspace-views.d.ts new file mode 100644 index 00000000000..754e637118e --- /dev/null +++ b/web/types/workspace-views.d.ts @@ -0,0 +1,24 @@ +import { IWorkspaceViewProps } from "./view-props"; + +export interface IWorkspaceView { + id: string; + access: string; + created_at: Date; + updated_at: Date; + is_favorite: boolean; + created_by: string; + updated_by: string; + name: string; + description: string; + query: any; + query_data: IWorkspaceViewProps; + project: string; + workspace: string; + workspace_detail?: { + id: string; + name: string; + slug: string; + }; +} + +export type TStaticViewTypes = "all-issues" | "assigned" | "created" | "subscribed"; diff --git a/apps/app/types/workspace.d.ts b/web/types/workspace.d.ts similarity index 84% rename from apps/app/types/workspace.d.ts rename to web/types/workspace.d.ts index 635aea2db65..1b9b924f4a9 100644 --- a/apps/app/types/workspace.d.ts +++ b/web/types/workspace.d.ts @@ -1,12 +1,6 @@ -import type { - IIssueFilterOptions, - IProjectMember, - IUser, - IUserMemberLite, - TIssueGroupByOptions, - TIssueOrderByOptions, - TIssueViewOptions, -} from "types"; +import type { IProjectMember, IUser, IUserLite, IUserMemberLite, IWorkspaceViewProps } from "types"; + +export type TUserWorkspaceRole = 5 | 10 | 15 | 20; export interface IWorkspace { readonly id: string; @@ -38,13 +32,13 @@ export interface IWorkspaceMemberInvitation { token: string; message: string; responded_at: Date; - role: 5 | 10 | 15 | 20; + role: TUserWorkspaceRole; created_by_detail: IUser; workspace: IWorkspace; } export interface IWorkspaceBulkInviteFormData { - emails: { email: string; role: 5 | 10 | 15 | 20 }[]; + emails: { email: string; role: TUserWorkspaceRole }[]; } export type Properties = { @@ -63,26 +57,30 @@ export type Properties = { updated_on: boolean; }; -export interface IWorkspaceViewProps { - properties: Properties; - issueView: TIssueViewOptions; - groupByProperty: TIssueGroupByOptions; - orderBy: TIssueOrderByOptions; - filters: Partial; - showEmptyGroups: boolean; -} - export interface IWorkspaceMember { - readonly id: string; - workspace: IWorkspace; - member: IUserMemberLite; - role: 5 | 10 | 15 | 20; company_role: string | null; - view_props: IWorkspaceViewProps; created_at: Date; + created_by: string; + id: string; + member: IUserLite; + role: TUserWorkspaceRole; updated_at: Date; + updated_by: string; + workspace: IWorkspaceLite; +} + +export interface IWorkspaceMemberMe { + company_role: string | null; + created_at: Date; created_by: string; + default_props: IWorkspaceViewProps; + id: string; + member: string; + role: TUserWorkspaceRole; + updated_at: Date; updated_by: string; + view_props: IWorkspaceViewProps; + workspace: string; } export interface ILastActiveWorkspaceDetails { diff --git a/yarn.lock b/yarn.lock index ac134d60f39..f25d1cf7d56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,46 +36,46 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" - integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== dependencies: - "@babel/highlight" "^7.22.10" + "@babel/highlight" "^7.22.13" chalk "^2.4.2" -"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" - integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9", "@babel/compat-data@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" + integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ== "@babel/core@^7.11.1": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.10.tgz#aad442c7bcd1582252cb4576747ace35bc122f35" - integrity sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw== + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" + integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" - "@babel/helper-compilation-targets" "^7.22.10" - "@babel/helper-module-transforms" "^7.22.9" - "@babel/helpers" "^7.22.10" - "@babel/parser" "^7.22.10" - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.10" - "@babel/types" "^7.22.10" - convert-source-map "^1.7.0" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helpers" "^7.23.2" + "@babel/parser" "^7.23.0" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.2.2" + json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" - integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== dependencies: - "@babel/types" "^7.22.10" + "@babel/types" "^7.23.0" "@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" @@ -88,32 +88,32 @@ "@babel/types" "^7.22.5" "@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz#573e735937e99ea75ea30788b57eb52fab7468c9" - integrity sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" + integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== dependencies: - "@babel/types" "^7.22.10" + "@babel/types" "^7.22.15" -"@babel/helper-compilation-targets@^7.22.10", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" - integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q== +"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" + integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== dependencies: "@babel/compat-data" "^7.22.9" - "@babel/helper-validator-option" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" browserslist "^4.21.9" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz#dd2612d59eac45588021ac3d6fa976d08f4e95a3" - integrity sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA== +"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" + integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" "@babel/helper-environment-visitor" "^7.22.5" "@babel/helper-function-name" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.15" "@babel/helper-optimise-call-expression" "^7.22.5" "@babel/helper-replace-supers" "^7.22.9" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" @@ -121,18 +121,18 @@ semver "^6.3.1" "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6" - integrity sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" + integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" regexpu-core "^5.3.1" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" - integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== +"@babel/helper-define-polyfill-provider@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz#a71c10f7146d809f4a256c373f462d9bba8cf6ba" + integrity sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -140,18 +140,18 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" - integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== +"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== -"@babel/helper-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" - integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== +"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== dependencies: - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" @@ -160,30 +160,30 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-member-expression-to-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2" - integrity sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ== +"@babel/helper-member-expression-to-functions@^7.22.15": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.23.0" -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" - integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.22.15" -"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" - integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== +"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e" + integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" "@babel/helper-simple-access" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" "@babel/helper-optimise-call-expression@^7.22.5": version "7.22.5" @@ -197,22 +197,22 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== -"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz#53a25b7484e722d7efb9c350c75c032d4628de82" - integrity sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ== +"@babel/helper-remap-async-to-generator@^7.22.20", "@babel/helper-remap-async-to-generator@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" + integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-wrap-function" "^7.22.9" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-wrap-function" "^7.22.20" "@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz#cbdc27d6d8d18cd22c81ae4293765a5d9afd0779" - integrity sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg== + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" + integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.22.15" "@babel/helper-optimise-call-expression" "^7.22.5" "@babel/helper-simple-access@^7.22.5": @@ -241,63 +241,63 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== -"@babel/helper-validator-identifier@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" - integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/helper-validator-option@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" - integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== +"@babel/helper-validator-option@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" + integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== -"@babel/helper-wrap-function@^7.22.9": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz#d845e043880ed0b8c18bd194a12005cb16d2f614" - integrity sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ== +"@babel/helper-wrap-function@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569" + integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw== dependencies: "@babel/helper-function-name" "^7.22.5" - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.10" + "@babel/template" "^7.22.15" + "@babel/types" "^7.22.19" -"@babel/helpers@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a" - integrity sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw== +"@babel/helpers@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767" + integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ== dependencies: - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.10" - "@babel/types" "^7.22.10" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" - integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== +"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== dependencies: - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.22.10", "@babel/parser@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55" - integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" - integrity sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz#02dc8a03f613ed5fdc29fb2f728397c78146c962" + integrity sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz#fef09f9499b1f1c930da8a0c419db42167d792ca" - integrity sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz#2aeb91d337d4e1a1e7ce85b76a37f5301781200f" + integrity sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.15" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" @@ -438,14 +438,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-async-generator-functions@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.10.tgz#45946cd17f915b10e65c29b8ed18a0a50fc648c8" - integrity sha512-eueE8lvKVzq5wIObKK/7dvoeKJ+xc6TvRn6aysIjS6pSCeLy7S/eVi7pEQknZqyqvzaNKdDtem8nUNTBgDVR2g== +"@babel/plugin-transform-async-generator-functions@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz#054afe290d64c6f576f371ccc321772c8ea87ebb" + integrity sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.9" + "@babel/helper-remap-async-to-generator" "^7.22.20" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-transform-async-to-generator@^7.22.5": @@ -464,10 +464,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-block-scoping@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz#88a1dccc3383899eb5e660534a76a22ecee64faa" - integrity sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg== +"@babel/plugin-transform-block-scoping@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz#8744d02c6c264d82e1a4bc5d2d501fd8aff6f022" + integrity sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -479,27 +479,27 @@ "@babel/helper-create-class-features-plugin" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-class-static-block@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz#3e40c46f048403472d6f4183116d5e46b1bff5ba" - integrity sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA== +"@babel/plugin-transform-class-static-block@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974" + integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.11" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-transform-classes@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz#e04d7d804ed5b8501311293d1a0e6d43e94c3363" - integrity sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ== +"@babel/plugin-transform-classes@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz#aaf4753aee262a232bbc95451b4bdf9599c65a0b" + integrity sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-environment-visitor" "^7.22.5" "@babel/helper-function-name" "^7.22.5" "@babel/helper-optimise-call-expression" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" "@babel/helper-split-export-declaration" "^7.22.6" globals "^11.1.0" @@ -511,10 +511,10 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/template" "^7.22.5" -"@babel/plugin-transform-destructuring@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz#38e2273814a58c810b6c34ea293be4973c4eb5e2" - integrity sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw== +"@babel/plugin-transform-destructuring@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz#6447aa686be48b32eaf65a73e0e2c0bd010a266c" + integrity sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -533,10 +533,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-dynamic-import@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz#d6908a8916a810468c4edff73b5b75bda6ad393e" - integrity sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ== +"@babel/plugin-transform-dynamic-import@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa" + integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" @@ -549,18 +549,18 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-export-namespace-from@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz#57c41cb1d0613d22f548fddd8b288eedb9973a5b" - integrity sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg== +"@babel/plugin-transform-export-namespace-from@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c" + integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-transform-for-of@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz#ab1b8a200a8f990137aff9a084f8de4099ab173f" - integrity sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A== +"@babel/plugin-transform-for-of@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz#f64b4ccc3a4f131a996388fae7680b472b306b29" + integrity sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -573,10 +573,10 @@ "@babel/helper-function-name" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-json-strings@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz#14b64352fdf7e1f737eed68de1a1468bd2a77ec0" - integrity sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A== +"@babel/plugin-transform-json-strings@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835" + integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-json-strings" "^7.8.3" @@ -588,10 +588,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-logical-assignment-operators@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz#66ae5f068fd5a9a5dc570df16f56c2a8462a9d6c" - integrity sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA== +"@babel/plugin-transform-logical-assignment-operators@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c" + integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" @@ -603,32 +603,32 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-modules-amd@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz#4e045f55dcf98afd00f85691a68fc0780704f526" - integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ== +"@babel/plugin-transform-modules-amd@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz#05b2bc43373faa6d30ca89214731f76f966f3b88" + integrity sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw== dependencies: - "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.0" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-modules-commonjs@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz#7d9875908d19b8c0536085af7b053fd5bd651bfa" - integrity sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA== +"@babel/plugin-transform-modules-commonjs@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz#b3dba4757133b2762c00f4f94590cf6d52602481" + integrity sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ== dependencies: - "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.0" "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-simple-access" "^7.22.5" -"@babel/plugin-transform-modules-systemjs@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz#18c31410b5e579a0092638f95c896c2a98a5d496" - integrity sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ== +"@babel/plugin-transform-modules-systemjs@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz#77591e126f3ff4132a40595a6cccd00a6b60d160" + integrity sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg== dependencies: "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.0" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" "@babel/plugin-transform-modules-umd@^7.22.5": version "7.22.5" @@ -653,32 +653,32 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-nullish-coalescing-operator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz#f8872c65776e0b552e0849d7596cddd416c3e381" - integrity sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA== +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc" + integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-transform-numeric-separator@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz#57226a2ed9e512b9b446517ab6fa2d17abb83f58" - integrity sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g== +"@babel/plugin-transform-numeric-separator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd" + integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-transform-object-rest-spread@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz#9686dc3447df4753b0b2a2fae7e8bc33cdc1f2e1" - integrity sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ== +"@babel/plugin-transform-object-rest-spread@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz#21a95db166be59b91cde48775310c0df6e1da56f" + integrity sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q== dependencies: - "@babel/compat-data" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.5" + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-parameters" "^7.22.15" "@babel/plugin-transform-object-super@^7.22.5": version "7.22.5" @@ -688,27 +688,27 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-replace-supers" "^7.22.5" -"@babel/plugin-transform-optional-catch-binding@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz#842080be3076703be0eaf32ead6ac8174edee333" - integrity sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg== +"@babel/plugin-transform-optional-catch-binding@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0" + integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.22.10", "@babel/plugin-transform-optional-chaining@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz#076d28a7e074392e840d4ae587d83445bac0372a" - integrity sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g== +"@babel/plugin-transform-optional-chaining@^7.22.15", "@babel/plugin-transform-optional-chaining@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz#73ff5fc1cf98f542f09f29c0631647d8ad0be158" + integrity sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-transform-parameters@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz#c3542dd3c39b42c8069936e48717a8d179d63a18" - integrity sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg== +"@babel/plugin-transform-parameters@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz#719ca82a01d177af358df64a514d64c2e3edb114" + integrity sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ== dependencies: "@babel/helper-plugin-utils" "^7.22.5" @@ -720,13 +720,13 @@ "@babel/helper-create-class-features-plugin" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-private-property-in-object@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz#07a77f28cbb251546a43d175a1dda4cf3ef83e32" - integrity sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ== +"@babel/plugin-transform-private-property-in-object@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1" + integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.11" "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" @@ -820,16 +820,16 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/preset-env@^7.11.0": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.10.tgz#3263b9fe2c8823d191d28e61eac60a79f9ce8a0f" - integrity sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A== + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.2.tgz#1f22be0ff0e121113260337dbc3e58fafce8d059" + integrity sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ== dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-compilation-targets" "^7.22.10" + "@babel/compat-data" "^7.23.2" + "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.15" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.15" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" @@ -850,41 +850,41 @@ "@babel/plugin-syntax-top-level-await" "^7.14.5" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" "@babel/plugin-transform-arrow-functions" "^7.22.5" - "@babel/plugin-transform-async-generator-functions" "^7.22.10" + "@babel/plugin-transform-async-generator-functions" "^7.23.2" "@babel/plugin-transform-async-to-generator" "^7.22.5" "@babel/plugin-transform-block-scoped-functions" "^7.22.5" - "@babel/plugin-transform-block-scoping" "^7.22.10" + "@babel/plugin-transform-block-scoping" "^7.23.0" "@babel/plugin-transform-class-properties" "^7.22.5" - "@babel/plugin-transform-class-static-block" "^7.22.5" - "@babel/plugin-transform-classes" "^7.22.6" + "@babel/plugin-transform-class-static-block" "^7.22.11" + "@babel/plugin-transform-classes" "^7.22.15" "@babel/plugin-transform-computed-properties" "^7.22.5" - "@babel/plugin-transform-destructuring" "^7.22.10" + "@babel/plugin-transform-destructuring" "^7.23.0" "@babel/plugin-transform-dotall-regex" "^7.22.5" "@babel/plugin-transform-duplicate-keys" "^7.22.5" - "@babel/plugin-transform-dynamic-import" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.11" "@babel/plugin-transform-exponentiation-operator" "^7.22.5" - "@babel/plugin-transform-export-namespace-from" "^7.22.5" - "@babel/plugin-transform-for-of" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.11" + "@babel/plugin-transform-for-of" "^7.22.15" "@babel/plugin-transform-function-name" "^7.22.5" - "@babel/plugin-transform-json-strings" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.11" "@babel/plugin-transform-literals" "^7.22.5" - "@babel/plugin-transform-logical-assignment-operators" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" "@babel/plugin-transform-member-expression-literals" "^7.22.5" - "@babel/plugin-transform-modules-amd" "^7.22.5" - "@babel/plugin-transform-modules-commonjs" "^7.22.5" - "@babel/plugin-transform-modules-systemjs" "^7.22.5" + "@babel/plugin-transform-modules-amd" "^7.23.0" + "@babel/plugin-transform-modules-commonjs" "^7.23.0" + "@babel/plugin-transform-modules-systemjs" "^7.23.0" "@babel/plugin-transform-modules-umd" "^7.22.5" "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" "@babel/plugin-transform-new-target" "^7.22.5" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5" - "@babel/plugin-transform-numeric-separator" "^7.22.5" - "@babel/plugin-transform-object-rest-spread" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" + "@babel/plugin-transform-numeric-separator" "^7.22.11" + "@babel/plugin-transform-object-rest-spread" "^7.22.15" "@babel/plugin-transform-object-super" "^7.22.5" - "@babel/plugin-transform-optional-catch-binding" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.22.10" - "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.11" + "@babel/plugin-transform-optional-chaining" "^7.23.0" + "@babel/plugin-transform-parameters" "^7.22.15" "@babel/plugin-transform-private-methods" "^7.22.5" - "@babel/plugin-transform-private-property-in-object" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.11" "@babel/plugin-transform-property-literals" "^7.22.5" "@babel/plugin-transform-regenerator" "^7.22.10" "@babel/plugin-transform-reserved-words" "^7.22.5" @@ -898,10 +898,10 @@ "@babel/plugin-transform-unicode-regex" "^7.22.5" "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" "@babel/preset-modules" "0.1.6-no-external-plugins" - "@babel/types" "^7.22.10" - babel-plugin-polyfill-corejs2 "^0.4.5" - babel-plugin-polyfill-corejs3 "^0.8.3" - babel-plugin-polyfill-regenerator "^0.5.2" + "@babel/types" "^7.23.0" + babel-plugin-polyfill-corejs2 "^0.4.6" + babel-plugin-polyfill-corejs3 "^0.8.5" + babel-plugin-polyfill-regenerator "^0.5.3" core-js-compat "^3.31.0" semver "^6.3.1" @@ -919,45 +919,45 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" - integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== +"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" - integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== +"@babel/template@^7.22.15", "@babel/template@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/parser" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" -"@babel/traverse@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa" - integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig== +"@babel/traverse@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== dependencies: - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.22.10" - "@babel/types" "^7.22.10" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.22.10", "@babel/types@^7.22.5", "@babel/types@^7.4.4": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" - integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg== +"@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.4.4": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== dependencies: "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" "@blueprintjs/colors@^4.2.1": @@ -967,6 +967,13 @@ dependencies: tslib "~2.5.0" +"@blueprintjs/colors@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@blueprintjs/colors/-/colors-5.0.5.tgz#3a8faa640dd2877aa4fd00b886cf8e58daf5f868" + integrity sha512-UcCsBxE8GTF6GW1oHBb+cuhPpKiJFWbIRkemwcRkp9HvXXQHxEaXlFFC6jAx5pf3JmRwde5/ck3r+lJFP1YqzA== + dependencies: + tslib "~2.6.2" + "@blueprintjs/core@^4.16.3", "@blueprintjs/core@^4.20.2": version "4.20.2" resolved "https://registry.yarnpkg.com/@blueprintjs/core/-/core-4.20.2.tgz#ae1bbaf13bd1bf887b506760c478cc940f6d6e20" @@ -984,6 +991,20 @@ react-transition-group "^4.4.5" tslib "~2.5.0" +"@blueprintjs/core@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@blueprintjs/core/-/core-5.6.0.tgz#915b708a8c2d7e916d30dae704829e2d948301d9" + integrity sha512-NtQL/iu8P8DhHUCWCstc9Ps+JkRZCPRJ2ZoxubOt21pfxN50CN0sKHkDETHUQyZ73RviveVIIK+m32mT5Wwdqg== + dependencies: + "@blueprintjs/colors" "^5.0.5" + "@blueprintjs/icons" "^5.3.0" + "@popperjs/core" "^2.11.7" + classnames "^2.3.1" + normalize.css "^8.0.1" + react-popper "^2.3.0" + react-transition-group "^4.4.5" + tslib "~2.6.2" + "@blueprintjs/icons@^4.16.0": version "4.16.0" resolved "https://registry.yarnpkg.com/@blueprintjs/icons/-/icons-4.16.0.tgz#47f9e8abe64d84fc18721080b8f191d8aac075d8" @@ -993,6 +1014,15 @@ classnames "^2.3.1" tslib "~2.5.0" +"@blueprintjs/icons@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@blueprintjs/icons/-/icons-5.3.0.tgz#9c9498df415bf7e028ceac15f90228d0ffd9a521" + integrity sha512-PGZHbWZ41b/SDOENlZQE1pAab4eluzf/hZ6sHB5nPrQNJuGNr94yaPp6u//Tu24iqVFFP20Soi3+ckhf/o3V/g== + dependencies: + change-case "^4.1.2" + classnames "^2.3.1" + tslib "~2.6.2" + "@blueprintjs/popover2@^1.13.3": version "1.14.11" resolved "https://registry.yarnpkg.com/@blueprintjs/popover2/-/popover2-1.14.11.tgz#0698fdeaf6710460cef0b71bed592ca37f40d1f9" @@ -1006,6 +1036,15 @@ react-popper "^2.3.0" tslib "~2.5.0" +"@blueprintjs/popover2@^2.0.10": + version "2.0.17" + resolved "https://registry.yarnpkg.com/@blueprintjs/popover2/-/popover2-2.0.17.tgz#ed1fd3a6fbb0a7ae623ccb649b150a7f22a09534" + integrity sha512-sJTlLD9ihKUITC8xEm1lzae4IumXkwoTEu/ajUTgd3GlLyQsUSC5CdlBihX7LlA60WbuyVSE3Jbazhqw2Fud2w== + dependencies: + "@blueprintjs/core" "^5.6.0" + classnames "^2.3.1" + tslib "~2.6.2" + "@cfcs/core@^0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@cfcs/core/-/core-0.0.6.tgz#9f8499dcd2ad29fd96d8fa72055411cd4a249121" @@ -1031,9 +1070,9 @@ "@egjs/list-differ" "^1.0.0" "@egjs/component@^3.0.2": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@egjs/component/-/component-3.0.4.tgz#ad7b53794b2a612806179a188ad828acb9525f61" - integrity sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@egjs/component/-/component-3.0.5.tgz#2dc86e835d5dc5055cdf46c7cd794eb45330e1b6" + integrity sha512-cLcGizTrrUNA2EYE3MBmEDt2tQv1joVP1Q3oDisZ5nw0MZDx2kcgEXM+/kZpfa/PAkFvYVhRUZwytIQWoN3V/w== "@egjs/list-differ@^1.0.0": version "1.0.1" @@ -1147,6 +1186,121 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== +"@esbuild/android-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" + integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== + +"@esbuild/android-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" + integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== + +"@esbuild/android-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" + integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== + +"@esbuild/darwin-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" + integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== + +"@esbuild/darwin-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" + integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== + +"@esbuild/freebsd-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" + integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== + +"@esbuild/freebsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" + integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== + +"@esbuild/linux-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" + integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== + +"@esbuild/linux-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" + integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== + +"@esbuild/linux-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" + integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== + +"@esbuild/linux-loong64@0.14.54": + version "0.14.54" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" + integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== + +"@esbuild/linux-loong64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" + integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== + +"@esbuild/linux-mips64el@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" + integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== + +"@esbuild/linux-ppc64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" + integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== + +"@esbuild/linux-riscv64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" + integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== + +"@esbuild/linux-s390x@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" + integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== + +"@esbuild/linux-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" + integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== + +"@esbuild/netbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" + integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== + +"@esbuild/openbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" + integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== + +"@esbuild/sunos-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" + integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== + +"@esbuild/win32-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" + integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== + +"@esbuild/win32-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" + integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== + +"@esbuild/win32-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" + integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1155,9 +1309,9 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": - version "4.6.2" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" - integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== "@eslint/eslintrc@^0.4.3": version "0.4.3" @@ -1189,10 +1343,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.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" @@ -1204,29 +1358,69 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^8.47.0": - version "8.47.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d" - integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og== +"@eslint/js@8.36.0": + version "8.36.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" + integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== -"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.3": - version "1.7.16" - resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.16.tgz#9c458c9c4dbb708258c9e8da3fe5363f915f7b11" - integrity sha512-2MphIAZdSUacZBT6EXk8AJkj+EuvaaJbtCyHTJrPsz8inhzCl7qeNPI1uk1AUvCgWylVtdN8cVVmnhUDPxPy3g== +"@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" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c" + integrity sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg== dependencies: - client-only "^0.0.1" + "@floating-ui/utils" "^0.1.3" -"@heroicons/react@^2.0.12": - version "2.0.18" - resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.18.tgz#f80301907c243df03c7e9fd76c0286e95361f7c1" - integrity sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw== +"@floating-ui/dom@^1.5.1": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa" + integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA== + dependencies: + "@floating-ui/core" "^1.4.2" + "@floating-ui/utils" "^0.1.3" -"@humanwhocodes/config-array@^0.11.10", "@humanwhocodes/config-array@^0.11.8": - version "0.11.10" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" - integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== +"@floating-ui/react-dom@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.2.tgz#fab244d64db08e6bed7be4b5fcce65315ef44d20" + integrity sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" + "@floating-ui/dom" "^1.5.1" + +"@floating-ui/utils@^0.1.3": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" + integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== + +"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.17", "@headlessui/react@^1.7.3": + version "1.7.17" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.17.tgz#a0ec23af21b527c030967245fd99776aa7352bc6" + integrity sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow== + dependencies: + client-only "^0.0.1" + +"@hello-pangea/dnd@^16.3.0": + version "16.3.0" + resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.3.0.tgz#3776212f812df4e8e69c42831ec8ab7ff3a087d6" + integrity sha512-RYQ/K8shtJoyNPvFWz0gfXIK7HF3P3mL9UZFGMuHB0ljRSXVgMjVFI/FxcZmakMzw6tO7NflWLriwTNBow/4vw== + dependencies: + "@babel/runtime" "^7.22.5" + css-box-model "^1.2.1" + memoize-one "^6.0.0" + raf-schd "^4.0.3" + react-redux "^8.1.1" + redux "^4.2.1" + use-memo-one "^1.1.3" + +"@humanwhocodes/config-array@^0.11.13", "@humanwhocodes/config-array@^0.11.8": + version "0.11.13" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" + integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== + dependencies: + "@humanwhocodes/object-schema" "^2.0.1" debug "^4.1.1" minimatch "^3.0.5" @@ -1244,11 +1438,16 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.0", "@humanwhocodes/object-schema@^1.2.1": +"@humanwhocodes/object-schema@^1.2.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/object-schema@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" + integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== + "@hypnosphi/create-react-context@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz#f8bfebdc7665f5d426cba3753e0e9c7d3154d7c6" @@ -1308,9 +1507,9 @@ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.19" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" - integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -1320,96 +1519,87 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== -"@mui/base@5.0.0-beta.10": - version "5.0.0-beta.10" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.10.tgz#b9a2de21c7de45aa4275c7ecf5aa803ecc236ba6" - integrity sha512-moTAhGwFfQffj7hsu61FnqcGqVcd53A1CrOhnskM9TF0Uh2rnLDMCuar4JRUWWpaJofAfQEbQBBFPadFQLI4PA== +"@mui/base@5.0.0-beta.22": + version "5.0.0-beta.22" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.22.tgz#9ea6be6c8bfc4d8f825660da36d228f5315d4706" + integrity sha512-l4asGID5tmyerx9emJfXOKLyXzaBtdXNIFE3M+IrSZaFtGFvaQKHhc3+nxxSxPf1+G44psjczM0ekRQCdXx9HA== dependencies: - "@babel/runtime" "^7.22.6" - "@emotion/is-prop-valid" "^1.2.1" - "@mui/types" "^7.2.4" - "@mui/utils" "^5.14.4" + "@babel/runtime" "^7.23.2" + "@floating-ui/react-dom" "^2.0.2" + "@mui/types" "^7.2.8" + "@mui/utils" "^5.14.16" "@popperjs/core" "^2.11.8" clsx "^2.0.0" prop-types "^15.8.1" - react-is "^18.2.0" - -"@mui/core-downloads-tracker@^5.14.4": - version "5.14.4" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.4.tgz#a517265647ee9660170107d68905db5e400576c5" - integrity sha512-pW2XghSi3hpYKX57Wu0SCWMTSpzvXZmmucj3TcOJWaCiFt4xr05w2gcwBZi36dAp9uvd9//9N51qbblmnD+GPg== -"@mui/icons-material@^5.14.1": - version "5.14.3" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.14.3.tgz#26a84d52ab2fceea2856adf7a139527b3a51ae90" - integrity sha512-XkxWPhageu1OPUm2LWjo5XqeQ0t2xfGe8EiLkRW9oz2LHMMZmijvCxulhgquUVTF1DnoSh+3KoDLSsoAFtVNVw== - dependencies: - "@babel/runtime" "^7.22.6" +"@mui/core-downloads-tracker@^5.14.16": + version "5.14.16" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.16.tgz#03ceb422d69a33e6c1cbd7e943cf60816878be2a" + integrity sha512-97isBjzH2v1K7oB4UH2f4NOkBShOynY6dhnoR2XlUk/g6bb7ZBv2I3D1hvvqPtpEigKu93e7f/jAYr5d9LOc5w== "@mui/material@^5.14.1": - version "5.14.4" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.14.4.tgz#9d4d1834a929a4acc59e550e34ca64c0fd60b3a6" - integrity sha512-2XUV3KyRC07BQPPzEgd+ss3x/ezXtHeKtOGCMCNmx3MauZojPYUpSwFkE0fYgYCD9dMQMVG4DY/VF38P0KShsg== - dependencies: - "@babel/runtime" "^7.22.6" - "@mui/base" "5.0.0-beta.10" - "@mui/core-downloads-tracker" "^5.14.4" - "@mui/system" "^5.14.4" - "@mui/types" "^7.2.4" - "@mui/utils" "^5.14.4" - "@types/react-transition-group" "^4.4.6" + version "5.14.16" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.14.16.tgz#45cd62d312d10399d3813ee6dc43bd1f11179bf4" + integrity sha512-W4zZ4vnxgGk6/HqBwgsDHKU7x2l2NhX+r8gAwfg58Rhu3ikfY7NkIS6y8Gl3NkATc4GG1FNaGjjpQKfJx3U6Jw== + dependencies: + "@babel/runtime" "^7.23.2" + "@mui/base" "5.0.0-beta.22" + "@mui/core-downloads-tracker" "^5.14.16" + "@mui/system" "^5.14.16" + "@mui/types" "^7.2.8" + "@mui/utils" "^5.14.16" + "@types/react-transition-group" "^4.4.8" clsx "^2.0.0" csstype "^3.1.2" prop-types "^15.8.1" react-is "^18.2.0" react-transition-group "^4.4.5" -"@mui/private-theming@^5.14.4": - version "5.14.4" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.14.4.tgz#0ec5510da05047f94984344360c7f457f3a0e19e" - integrity sha512-ISXsHDiQ3z1XA4IuKn+iXDWvDjcz/UcQBiFZqtdoIsEBt8CB7wgdQf3LwcwqO81dl5ofg/vNQBEnXuKfZHrnYA== +"@mui/private-theming@^5.14.16": + version "5.14.16" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.14.16.tgz#ffdc9a9d3deaa46af000f04c0a9cc3a982f73071" + integrity sha512-FNlL0pTSEBh8nXsVWreCHDSHk+jG8cBx1sxRbT8JVtL+PYbYPi802zfV4B00Kkf0LNRVRvAVQwojMWSR/MYGng== dependencies: - "@babel/runtime" "^7.22.6" - "@mui/utils" "^5.14.4" + "@babel/runtime" "^7.23.2" + "@mui/utils" "^5.14.16" prop-types "^15.8.1" -"@mui/styled-engine@^5.13.2": - version "5.13.2" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.13.2.tgz#c87bd61c0ab8086d34828b6defe97c02bcd642ef" - integrity sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw== +"@mui/styled-engine@^5.14.16": + version "5.14.16" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.14.16.tgz#a4a78a9980f138c2e705d04d67d44051f5005f22" + integrity sha512-FfvYvTG/Zd+KXMMImbcMYEeQAbONGuX5Vx3gBmmtB6KyA7Mvm9Pma1ly3R0gc44yeoFd+2wBjn1feS8h42HW5w== dependencies: - "@babel/runtime" "^7.21.0" + "@babel/runtime" "^7.23.2" "@emotion/cache" "^11.11.0" csstype "^3.1.2" prop-types "^15.8.1" -"@mui/system@^5.14.4": - version "5.14.4" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.14.4.tgz#306a2fdd41ab6f4912ea689316f834db8461bb86" - integrity sha512-oPgfWS97QNfHcDBapdkZIs4G5i85BJt69Hp6wbXF6s7vi3Evcmhdk8AbCRW6n0sX4vTj8oe0mh0RIm1G2A1KDA== +"@mui/system@^5.14.16": + version "5.14.16" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.14.16.tgz#5c30c5123767416358c3b73774eb985e189119a4" + integrity sha512-uKnPfsDqDs8bbN54TviAuoGWOmFiQLwNZ3Wvj+OBkJCzwA6QnLb/sSeCB7Pk3ilH4h4jQ0BHtbR+Xpjy9wlOuA== dependencies: - "@babel/runtime" "^7.22.6" - "@mui/private-theming" "^5.14.4" - "@mui/styled-engine" "^5.13.2" - "@mui/types" "^7.2.4" - "@mui/utils" "^5.14.4" + "@babel/runtime" "^7.23.2" + "@mui/private-theming" "^5.14.16" + "@mui/styled-engine" "^5.14.16" + "@mui/types" "^7.2.8" + "@mui/utils" "^5.14.16" clsx "^2.0.0" csstype "^3.1.2" prop-types "^15.8.1" -"@mui/types@^7.2.4": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.4.tgz#b6fade19323b754c5c6de679a38f068fd50b9328" - integrity sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA== +"@mui/types@^7.2.8": + version "7.2.8" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.8.tgz#2ed4402f104d65fcd4f460ca358654c8935e2285" + integrity sha512-9u0ji+xspl96WPqvrYJF/iO+1tQ1L5GTaDOeG3vCR893yy7VcWwRNiVMmPdPNpMDqx0WV1wtEW9OMwK9acWJzQ== -"@mui/utils@^5.14.4": - version "5.14.4" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.14.4.tgz#498cc2b08e46776c1e4327bfd8c23ad2f5890bc6" - integrity sha512-4ANV0txPD3x0IcTCSEHKDWnsutg1K3m6Vz5IckkbLXVYu17oOZCVUdOKsb/txUmaCd0v0PmSRe5PW+Mlvns5dQ== +"@mui/utils@^5.14.16": + version "5.14.16" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.14.16.tgz#09a15fd45530cadc642c5c08eb6cc660ea230506" + integrity sha512-3xV31GposHkwRbQzwJJuooWpK2ybWdEdeUPtRjv/6vjomyi97F3+68l+QVj9tPTvmfSbr2sx5c/NuvDulrdRmA== dependencies: - "@babel/runtime" "^7.22.6" - "@types/prop-types" "^15.7.5" - "@types/react-is" "^18.2.1" + "@babel/runtime" "^7.23.2" + "@types/prop-types" "^15.7.9" prop-types "^15.8.1" react-is "^18.2.0" @@ -1418,11 +1608,6 @@ resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.2.tgz#fb819366771f5721e9438ca3a42ad18684f0949b" integrity sha512-upwtMaHxlv/udAWGq0kE+rg8huwmcxQPsKZFhS1R5iVO323mvxEBe1YrSXe1awLbg9sTIuEHbgxjLLt7JbeuAQ== -"@next/env@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.16.tgz#382b565b35a2a69bd0e6b50f74c7b95f0c4b1097" - integrity sha512-pCU0sJBqdfKP9mwDadxvZd+eLz3fZrTlmmDHY12Hdpl3DD0vy8ou5HWKVfG0zZS6tqhL4wnQqRbspdY5nqa7MA== - "@next/eslint-plugin-next@12.2.2": version "12.2.2" resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.2.2.tgz#b4a22c06b6454068b54cc44502168d90fbb29a6d" @@ -1444,6 +1629,13 @@ dependencies: glob "7.1.7" +"@next/eslint-plugin-next@13.2.4": + version "13.2.4" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.2.4.tgz#3e124cd10ce24dab5d3448ce04104b4f1f4c6ca7" + integrity sha512-ck1lI+7r1mMJpqLNa3LJ5pxCfOB1lfJncKmRJeJxcJqcngaFwylreLP7da6Rrjr6u2gVRTfmnkSkjc80IiQCwQ== + dependencies: + glob "7.1.7" + "@next/swc-android-arm-eabi@12.3.2": version "12.3.2" resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.2.tgz#806e3be9741bc14aafdfad0f0c4c6a8de5b77ee1" @@ -1459,21 +1651,11 @@ resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.2.tgz#97c532d35c66ce6b6941ae24b5b8b267b9b0d0d8" integrity sha512-PTUfe1ZrwjsiuTmr3bOM9lsoy5DCmfYsLOUF9ZVhtbi5MNJVmUTy4VZ06GfrvnCO5hGCr48z3vpFE9QZ0qLcPw== -"@next/swc-darwin-arm64@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.16.tgz#ed6a342f95e5f21213fdadbceb65b40ae678cee0" - integrity sha512-Rl6i1uUq0ciRa3VfEpw6GnWAJTSKo9oM2OrkGXPsm7rMxdd2FR5NkKc0C9xzFCI4+QtmBviWBdF2m3ur3Nqstw== - "@next/swc-darwin-x64@12.3.2": version "12.3.2" resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.2.tgz#e0cb4ff4b11faaff3a891bd1d18ed72f71e30ebe" integrity sha512-1HkjmS9awwlaeEY8Y01nRSNkSv3y+qnC/mjMPe/W66hEh3QKa/LQHqHeS7NOdEs19B2mhZ7w+EgMRXdLQ0Su8w== -"@next/swc-darwin-x64@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.16.tgz#36c16066a1a3ef8211e84a6a5d72bef15826b291" - integrity sha512-o1vIKYbZORyDmTrPV1hApt9NLyWrS5vr2p5hhLGpOnkBY1cz6DAXjv8Lgan8t6X87+83F0EUDlu7klN8ieZ06A== - "@next/swc-freebsd-x64@12.3.2": version "12.3.2" resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.2.tgz#d7b93dd344cb67d1969565d0796c7b7d0217fccf" @@ -1489,71 +1671,36 @@ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.2.tgz#26df7d7cdc18cf413f12a408179ee4ac315f383a" integrity sha512-T9GCFyOIb4S3acA9LqflUYD+QZ94iZketHCqKdoO0Nx0OCHIgGJV5rotDe8TDXwh/goYpIfyHU4j1qqw4w4VnA== -"@next/swc-linux-arm64-gnu@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.16.tgz#a5b5500737f07e3aa7f184014d8df7973420df26" - integrity sha512-JRyAl8lCfyTng4zoOmE6hNI2f1MFUr7JyTYCHl1RxX42H4a5LMwJhDVQ7a9tmDZ/yj+0hpBn+Aan+d6lA3v0UQ== - "@next/swc-linux-arm64-musl@12.3.2": version "12.3.2" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.2.tgz#fd42232a6b10d9f9a4f71433d59c280a4532d06f" integrity sha512-hxNVZS6L3c2z3l9EH2GP0MGQ9exu6O8cohYNZyqC9WUl6C03sEn8xzDH1y+NgD3fVurvYkGU5F0PDddJJLfDIw== -"@next/swc-linux-arm64-musl@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.16.tgz#381b7662c5b10ed5750dce41dd57841aa0713e77" - integrity sha512-9gqVqNzUMWbUDgDiND18xoUqhwSm2gmksqXgCU0qaOKt6oAjWz8cWYjgpPVD0WICKFylEY/gvPEP1fMZDVFZ/g== - "@next/swc-linux-x64-gnu@12.3.2": version "12.3.2" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.2.tgz#5307579e3d8fbdb03adbe6cfc915b51548e0a103" integrity sha512-fCPkLuwDwY8/QeXxciJJjDHG09liZym/Bhb4A+RLFQ877wUkwFsNWDUTSdUx0YXlYK/1gf67BKauqKkOKp6CYw== -"@next/swc-linux-x64-gnu@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.16.tgz#6e0b0eab1c316506950aeb4a09a5ea5c38edabe7" - integrity sha512-KcQGwchAKmZVPa8i5PLTxvTs1/rcFnSltfpTm803Tr/BtBV3AxCkHLfhtoyVtVzx/kl/oue8oS+DSmbepQKwhw== - "@next/swc-linux-x64-musl@12.3.2": version "12.3.2" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.2.tgz#d5cb920a825a8dc80ffba8a6b797fb845af0b84c" integrity sha512-o+GifBIQ2K+/MEFxHsxUZoU3bsuVFLXZYWd3idimFHiVdDCVYiKsY6mYMmKDlucX+9xRyOCkKL9Tjf+3tuXJpw== -"@next/swc-linux-x64-musl@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.16.tgz#36b84e4509168a5cadf9dfd728c239002d4311fe" - integrity sha512-2RbMZNxYnJmW8EPHVBsGZPq5zqWAyBOc/YFxq/jIQ/Yn3RMFZ1dZVCjtIcsiaKmgh7mjA/W0ApbumutHNxRqqQ== - "@next/swc-win32-arm64-msvc@12.3.2": version "12.3.2" resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.2.tgz#2a0d619e5bc0cec17ed093afd1ca6b1c37c2690c" integrity sha512-crii66irzGGMSUR0L8r9+A06eTv7FTXqw4rgzJ33M79EwQJOdpY7RVKXLQMurUhniEeQEEOfamiEdPIi/qxisw== -"@next/swc-win32-arm64-msvc@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.16.tgz#52d36f909ccdefa2761617b6d4e9ae65f99880a9" - integrity sha512-thDcGonELN7edUKzjzlHrdoKkm7y8IAdItQpRvvMxNUXa4d9r0ElofhTZj5emR7AiXft17hpen+QAkcWpqG7Jg== - "@next/swc-win32-ia32-msvc@12.3.2": version "12.3.2" resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.2.tgz#769bef60d0d678c3d7606a4dc7fee018d6199227" integrity sha512-5hRUSvn3MdQ4nVRu1rmKxq5YJzpTtZfaC/NyGw6wa4NSF1noUn/pdQGUr+I5Qz3CZkd1gZzzC0eaXQHlrk0E2g== -"@next/swc-win32-ia32-msvc@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.16.tgz#a9cb0556d19c33fbb39ac9bef195fd490d6c7673" - integrity sha512-f7SE1Mo4JAchUWl0LQsbtySR9xCa+x55C0taetjUApKtcLR3AgAjASrrP+oE1inmLmw573qRnE1eZN8YJfEBQw== - "@next/swc-win32-x64-msvc@12.3.2": version "12.3.2" resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.2.tgz#45beb4b9d28e6dd6abf63cab1c5b92dc84323a6b" integrity sha512-tpQJYUH+TzPMIsdVl9fH8uDg47iwiNjKY+8e9da3dXqlkztKzjSw0OwSADoqh3KrifplXeKSta+BBGLdBqg3sg== -"@next/swc-win32-x64-msvc@13.4.16": - version "13.4.16" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.16.tgz#79a151d94583e03992c80df3d3e7f7686390ddac" - integrity sha512-WamDZm1M/OEM4QLce3lOmD1XdLEl37zYZwlmOLhmF7qYJ2G6oYm9+ejZVv+LakQIsIuXhSpVlOvrxIAHqwRkPQ== - "@nivo/annotations@0.80.0": version "0.80.0" resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.80.0.tgz#127e4801fff7370dcfb9acfe1e335781dd65cfd5" @@ -2011,18 +2158,18 @@ picomatch "^2.2.2" "@rollup/pluginutils@^5.0.1": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.3.tgz#183126d69aeb1cfa23401d5a71cb4b8c16c4a4e0" - integrity sha512-hfllNN4a80rwNQ9QCxhxuHCGHMAvabXqxNdaChUSSadMre7t4iEUI6fFAhBOn/eIYTgYVhBv7vCLsAJ4u3lf3g== + version "5.0.5" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz#bbb4c175e19ebfeeb8c132c2eea0ecb89941a66c" + integrity sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q== dependencies: "@types/estree" "^1.0.0" estree-walker "^2.0.2" picomatch "^2.3.1" "@rushstack/eslint-patch@^1.1.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69" - integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw== + version "1.5.1" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz#5f1b518ec5fa54437c0b7c4a821546c64fed6922" + integrity sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA== "@scena/dragscroll@^1.4.0": version "1.4.0" @@ -2046,32 +2193,30 @@ dependencies: "@daybrush/utils" "^1.4.0" -"@sentry-internal/tracing@7.63.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.63.0.tgz#58903b2205456034611cc5bc1b5b2479275f89c7" - integrity sha512-Fxpc53p6NGvLSURg3iRvZA0k10K9yfeVhtczvJnpX30POBuV41wxpkLHkb68fjksirjEma1K3Ut1iLOEEDpPQg== +"@sentry-internal/tracing@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.77.0.tgz#f3d82486f8934a955b3dd2aa54c8d29586e42a37" + integrity sha512-8HRF1rdqWwtINqGEdx8Iqs9UOP/n8E0vXUu3Nmbqj4p5sQPA7vvCfq+4Y4rTqZFc7sNdFpDsRION5iQEh8zfZw== dependencies: - "@sentry/core" "7.63.0" - "@sentry/types" "7.63.0" - "@sentry/utils" "7.63.0" - tslib "^2.4.1 || ^1.9.3" + "@sentry/core" "7.77.0" + "@sentry/types" "7.77.0" + "@sentry/utils" "7.77.0" -"@sentry/browser@7.63.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.63.0.tgz#d7eee4be7bfff015f050bca83cafb111dc13d40d" - integrity sha512-P1Iw/2281C/7CUCRsN4jgXvjMNKnrwKqxRg7JqN8eVeCDPMpOeEPHNJ6YatEXdVLTKVn0JB7L63Q1prhFr8+SQ== +"@sentry/browser@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.77.0.tgz#155440f1a0d3a1bbd5d564c28d6b0c9853a51d72" + integrity sha512-nJ2KDZD90H8jcPx9BysQLiQW+w7k7kISCWeRjrEMJzjtge32dmHA8G4stlUTRIQugy5F+73cOayWShceFP7QJQ== dependencies: - "@sentry-internal/tracing" "7.63.0" - "@sentry/core" "7.63.0" - "@sentry/replay" "7.63.0" - "@sentry/types" "7.63.0" - "@sentry/utils" "7.63.0" - tslib "^2.4.1 || ^1.9.3" + "@sentry-internal/tracing" "7.77.0" + "@sentry/core" "7.77.0" + "@sentry/replay" "7.77.0" + "@sentry/types" "7.77.0" + "@sentry/utils" "7.77.0" "@sentry/cli@^1.74.6": - version "1.75.2" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.75.2.tgz#2c38647b38300e52c9839612d42b7c23f8d6455b" - integrity sha512-CG0CKH4VCKWzEaegouWfCLQt9SFN+AieFESCatJ7zSuJmzF05ywpMusjxqRul6lMwfUhRKjGKOzcRJ1jLsfTBw== + version "1.76.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.76.0.tgz#3d48248a4fec2fee7c4d30320c563c91c75290ec" + integrity sha512-56bVyUJoi52dop/rFEaSoU4AfVRXpR6M+nZBwN1iGUAwdfBrarNbtmIOjfgPi+tVzVB5ck09PzVXG6zeBqJJcA== dependencies: https-proxy-agent "^5.0.0" mkdirp "^0.5.5" @@ -2080,89 +2225,94 @@ proxy-from-env "^1.1.0" which "^2.0.2" -"@sentry/core@7.63.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.63.0.tgz#8c38da6ef3a1de6e364463a09bc703b196ecbba4" - integrity sha512-13Ljiq8hv6ieCkO+Am99/PljYJO5ynKT/hRQrWgGy9IIEgUr8sV3fW+1W6K4/3MCeOJou0HsiGBjOD1mASItVg== +"@sentry/core@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.77.0.tgz#21100843132beeeff42296c8370cdcc7aa1d8510" + integrity sha512-Tj8oTYFZ/ZD+xW8IGIsU6gcFXD/gfE+FUxUaeSosd9KHwBQNOLhZSsYo/tTVf/rnQI/dQnsd4onPZLiL+27aTg== dependencies: - "@sentry/types" "7.63.0" - "@sentry/utils" "7.63.0" - tslib "^2.4.1 || ^1.9.3" + "@sentry/types" "7.77.0" + "@sentry/utils" "7.77.0" -"@sentry/integrations@7.63.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.63.0.tgz#bf4268b524670fdbc290dc489de0069143b499c6" - integrity sha512-+P8GNqFZNH/yS/KPbvUfUDERneoRNUrqp9ayvvp8aq4cTtrBdM72CYgI21oG6cti42SSM1VDLYZomTV3ElPzSg== +"@sentry/integrations@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.77.0.tgz#f2717e05cb7c69363316ccd34096b2ea07ae4c59" + integrity sha512-P055qXgBHeZNKnnVEs5eZYLdy6P49Zr77A1aWJuNih/EenzMy922GOeGy2mF6XYrn1YJSjEwsNMNsQkcvMTK8Q== dependencies: - "@sentry/types" "7.63.0" - "@sentry/utils" "7.63.0" + "@sentry/core" "7.77.0" + "@sentry/types" "7.77.0" + "@sentry/utils" "7.77.0" localforage "^1.8.1" - tslib "^2.4.1 || ^1.9.3" "@sentry/nextjs@^7.36.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.63.0.tgz#79bca799d451e1570c7873474e295660218cadea" - integrity sha512-pf1kEt2oqxe84+DdmGkI6BEe1KMUcUFU4PZKg5GRFY7e2ZqHoS8hTJF5rBkScqVlQoXDTiGpfI+vU8Ie3snUcQ== + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.77.0.tgz#036b1c45dd106e01d44967c97985464e108922be" + integrity sha512-8tYPBt5luFjrng1sAMJqNjM9sq80q0jbt6yariADU9hEr7Zk8YqFaOI2/Q6yn9dZ6XyytIRtLEo54kk2AO94xw== dependencies: "@rollup/plugin-commonjs" "24.0.0" - "@sentry/core" "7.63.0" - "@sentry/integrations" "7.63.0" - "@sentry/node" "7.63.0" - "@sentry/react" "7.63.0" - "@sentry/types" "7.63.0" - "@sentry/utils" "7.63.0" + "@sentry/core" "7.77.0" + "@sentry/integrations" "7.77.0" + "@sentry/node" "7.77.0" + "@sentry/react" "7.77.0" + "@sentry/types" "7.77.0" + "@sentry/utils" "7.77.0" + "@sentry/vercel-edge" "7.77.0" "@sentry/webpack-plugin" "1.20.0" chalk "3.0.0" + resolve "1.22.8" rollup "2.78.0" stacktrace-parser "^0.1.10" - tslib "^2.4.1 || ^1.9.3" - -"@sentry/node@7.63.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.63.0.tgz#38508a440c04c0e98d00f5a1855e5448ee70c8d6" - integrity sha512-tSMyfQNbfjX1w8vJDZtvWeaD4QQ/Z4zVW/TLXfL/JZFIIksPgDZmqLdF+NJS4bSGTU5JiHiUh4pYhME4mHgNBQ== - dependencies: - "@sentry-internal/tracing" "7.63.0" - "@sentry/core" "7.63.0" - "@sentry/types" "7.63.0" - "@sentry/utils" "7.63.0" - cookie "^0.4.1" + +"@sentry/node@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.77.0.tgz#a247452779a5bcb55724457707286e3e4a29dbbe" + integrity sha512-Ob5tgaJOj0OYMwnocc6G/CDLWC7hXfVvKX/ofkF98+BbN/tQa5poL+OwgFn9BA8ud8xKzyGPxGU6LdZ8Oh3z/g== + dependencies: + "@sentry-internal/tracing" "7.77.0" + "@sentry/core" "7.77.0" + "@sentry/types" "7.77.0" + "@sentry/utils" "7.77.0" https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^2.4.1 || ^1.9.3" -"@sentry/react@7.63.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.63.0.tgz#6d318191e13ccf7ebba4897d7258b4ea3bcf6c51" - integrity sha512-KFRjgADVE4aMI7gJmGnoSz65ZErQlz9xRB3vETWSyNOLprWXuQLPPtcDEn39BROtsDG4pLyYFaSDiD7o0+DyjQ== +"@sentry/react@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.77.0.tgz#9da14e4b21eae4b5a6306d39bb7c42ef0827d2c2" + integrity sha512-Q+htKzib5em0MdaQZMmPomaswaU3xhcVqmLi2CxqQypSjbYgBPPd+DuhrXKoWYLDDkkbY2uyfe4Lp3yLRWeXYw== dependencies: - "@sentry/browser" "7.63.0" - "@sentry/types" "7.63.0" - "@sentry/utils" "7.63.0" + "@sentry/browser" "7.77.0" + "@sentry/types" "7.77.0" + "@sentry/utils" "7.77.0" hoist-non-react-statics "^3.3.2" - tslib "^2.4.1 || ^1.9.3" -"@sentry/replay@7.63.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.63.0.tgz#989ae32ea028a5eca323786cc07294fedb1f0d45" - integrity sha512-ikeFVojuP9oDF103blZcj0Vvb4S50dV54BESMrMW2lYBoMMjvOd7AdL+iDHjn1OL05/mv1C6Oc8MovmvdjILVA== +"@sentry/replay@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.77.0.tgz#21d242c9cd70a7235237216174873fd140b6eb80" + integrity sha512-M9Ik2J5ekl+C1Och3wzLRZVaRGK33BlnBwfwf3qKjgLDwfKW+1YkwDfTHbc2b74RowkJbOVNcp4m8ptlehlSaQ== dependencies: - "@sentry/core" "7.63.0" - "@sentry/types" "7.63.0" - "@sentry/utils" "7.63.0" + "@sentry-internal/tracing" "7.77.0" + "@sentry/core" "7.77.0" + "@sentry/types" "7.77.0" + "@sentry/utils" "7.77.0" -"@sentry/types@7.63.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.63.0.tgz#8032029fee6f70e04b667646626a674b03e2f79b" - integrity sha512-pZNwJVW7RqNLGuTUAhoygt0c9zmc0js10eANAz0MstygJRhQI1tqPDuiELVdujPrbeL+IFKF+7NvRDAydR2Niw== +"@sentry/types@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.77.0.tgz#c5d00fe547b89ccde59cdea59143bf145cee3144" + integrity sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA== -"@sentry/utils@7.63.0": - version "7.63.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.63.0.tgz#7c598553b4dbb6e3740dc96bc7f112ec32edbe69" - integrity sha512-7FQv1RYAwnuTuarruP+1+Jd6YQuN7i/Y7KltwPMVEwU7j5mzYQaexLr/Jz1XIdR2KYVdkbXQyP8jj8BmA6u9Jw== +"@sentry/utils@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.77.0.tgz#1f88501f0b8777de31b371cf859d13c82ebe1379" + integrity sha512-NmM2kDOqVchrey3N5WSzdQoCsyDkQkiRxExPaNI2oKQ/jMWHs9yt0tSy7otPBcXs0AP59ihl75Bvm1tDRcsp5g== dependencies: - "@sentry/types" "7.63.0" - tslib "^2.4.1 || ^1.9.3" + "@sentry/types" "7.77.0" + +"@sentry/vercel-edge@7.77.0": + version "7.77.0" + resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-7.77.0.tgz#6a90a869878e4e78803c4331c30aea841fcc6a73" + integrity sha512-ffddPCgxVeAccPYuH5sooZeHBqDuJ9OIhIRYKoDi4TvmwAzWo58zzZWhRpkHqHgIQdQvhLVZ5F+FSQVWnYSOkw== + dependencies: + "@sentry/core" "7.77.0" + "@sentry/types" "7.77.0" + "@sentry/utils" "7.77.0" "@sentry/webpack-plugin@1.20.0": version "1.20.0" @@ -2189,195 +2339,201 @@ dependencies: tslib "^2.4.0" -"@swc/helpers@0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" - integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== - dependencies: - tslib "^2.4.0" - "@tailwindcss/typography@^0.5.9": - version "0.5.9" - resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.9.tgz#027e4b0674929daaf7c921c900beee80dbad93e8" - integrity sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg== + version "0.5.10" + resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.10.tgz#2abde4c6d5c797ab49cf47610830a301de4c1e0a" + integrity sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw== dependencies: lodash.castarray "^4.4.0" lodash.isplainobject "^4.0.6" lodash.merge "^4.6.2" postcss-selector-parser "6.0.10" -"@tiptap-pro/extension-unique-id@^2.1.0": - version "2.1.0" - resolved "https://registry.tiptap.dev/@tiptap-pro%2fextension-unique-id/-/extension-unique-id-2.1.0.tgz#7f5e7cc2d068eff11531e2b8c894ddf13c8d41d8" - integrity sha512-RdkDqFV0adN/NXJ31I64mD3VmoGhQfNMmOyF5X92fbIymHaonAGzDeoXoindd+6MUUd6e3cm75x5VQLdsddG4Q== - dependencies: - uuid "^8.3.2" - -"@tiptap/core@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.4.tgz#0a2047150ae537e75f96841a603699526f7b4ec5" - integrity sha512-2YOMjRqoBGEP4YGgYpuPuBBJHMeqKOhLnS0WVwjVP84zOmMgZ7A8M6ILC9Xr7Q/qHZCvyBGWOSsI7+3HsEzzYQ== +"@tiptap/core@^2.1.12", "@tiptap/core@^2.1.7": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.12.tgz#904fdf147e91b5e60561c76e7563c1b5a32f54ab" + integrity sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ== -"@tiptap/extension-blockquote@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.4.tgz#1e87f8f157573deec65b54d8a8b5568d79066a86" - integrity sha512-z5qfuLi04OgCBI6/odzB2vhulT/wpjymYOnON65vLXGZZbUw4cbPloykhqgWvQp+LzKH+HBhl4fz53d5CgnbOA== +"@tiptap/extension-blockquote@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.1.12.tgz#97b43419606acf9bfd93b9f482a1827dcac8c3e9" + integrity sha512-Qb3YRlCfugx9pw7VgLTb+jY37OY4aBJeZnqHzx4QThSm13edNYjasokbX0nTwL1Up4NPTcY19JUeHt6fVaVVGg== -"@tiptap/extension-bold@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.4.tgz#debba8b0d957fe0b6943354834d8f1f0f8c0695c" - integrity sha512-CWSQy1uWkVsen8HUsqhm+oEIxJrCiCENABUbhaVcJL/MqhnP4Trrh1B6O00Yfoc0XToPRRibDaHMFs4A3MSO0g== +"@tiptap/extension-bold@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.1.12.tgz#5dbf41105fc0fbde8adbff629312187fbebc39b0" + integrity sha512-AZGxIxcGU1/y6V2YEbKsq6BAibL8yQrbRm6EdcBnby41vj1WziewEKswhLGmZx5IKM2r2ldxld03KlfSIlKQZg== -"@tiptap/extension-bubble-menu@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.4.tgz#f9dde09d3984e9879b1fe13d3e8c1859f0779ef5" - integrity sha512-+cRZwj0YINNNDElSAiX1pvY2K98S2j9MQW2dXV5oLqsJhqGPZsKxVo8I1u7ZtqUla3QE1V18RYPAzVgTiMRkBg== +"@tiptap/extension-bubble-menu@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.1.12.tgz#4103a21a6433e58690c8f742ece39fad78dc26eb" + integrity sha512-gAGi21EQ4wvLmT7klgariAc2Hf+cIjaNU2NWze3ut6Ku9gUo5ZLqj1t9SKHmNf4d5JG63O8GxpErqpA7lHlRtw== dependencies: tippy.js "^6.3.7" -"@tiptap/extension-bullet-list@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.4.tgz#d192767d39e45253c5e9d974e949f271e09c72d7" - integrity sha512-JSZKBVTaKSuLl5fR4EKE4dOINOrgeRHYA25Vj6cWjgdvpTw5ef7vcUdn9yP4JwTmLRI+VnnMlYL3rqigU3iZNg== - -"@tiptap/extension-code-block-lowlight@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.0.4.tgz#e641f08d2ea77271722e848c6efa819b63638b1a" - integrity sha512-fKM/4MY9R75IJJVt7P+aD+GX3yzzL6oHo1dn4hNFJlYp2x5+yH6kneaqKcTglVicBCGc8Ks6wJLEZTxxG35MOA== - -"@tiptap/extension-code-block@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.0.4.tgz#d551ee7c13fef379bbbad298f1be757d1125cd54" - integrity sha512-In2tV3rgm/MznVF0N7qYsYugPWSzhZHaCRCWcFKNvllMExpo91bUWvk+hXaIhhPxvuqGIVezjybwrYuU3bJW0g== - -"@tiptap/extension-code@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.0.4.tgz#6952d402e7372dd2d129e962bf9bd54d68ee6183" - integrity sha512-HuwJSJkipZf4hkns9witv1CABNIPiB9C8lgAQXK4xJKcoUQChcnljEL+PQ2NqeEeMTEeV3nG3A/0QafH0pgTgg== - -"@tiptap/extension-color@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-color/-/extension-color-2.0.4.tgz#564265c2bcadd268e6b5745d2a06571744cb4090" - integrity sha512-7Eb5Gk9v3sj2i1Q8dfqmpnc5aDPC/t0ZEsSLRi4C6SNo1nBeUxteXzpzxWwYjTvK+Um40STR89Z6PY14FIYXSA== - -"@tiptap/extension-document@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.4.tgz#f94e6da23a7d93a8ea34c6767d4e2e31f5ab8849" - integrity sha512-mCj2fAhnNhIHttPSqfTPSSTGwClGaPYvhT56Ij/Pi4iCrWjPXzC4XnIkIHSS34qS2tJN4XJzr/z7lm3NeLkF1w== - -"@tiptap/extension-dropcursor@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.4.tgz#f4a7542866c9100fee8e78eca5eebefff58989ca" - integrity sha512-1OmKBv/E+nJo2vsosvu8KwFiBB+gZM1pY61qc7JbwEKHSYAxUFHfvLkIA0IQ53Z0DHMrFSKgWmHEcbnqtGevCA== - -"@tiptap/extension-floating-menu@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.4.tgz#82f12c2415b7ddbfd782a03b100f717e9905bab0" - integrity sha512-0YRE738k+kNKuSHhAb3jj9ZQ7Kda78RYRr+cX2jrQVueIMKebPIY07eBt6JcKmob9V9vcNn9qLtBfmygfcPUQg== +"@tiptap/extension-bullet-list@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.12.tgz#7c905a577ce30ef2cb335870a23f9d24fd26f6aa" + integrity sha512-vtD8vWtNlmAZX8LYqt2yU9w3mU9rPCiHmbp4hDXJs2kBnI0Ju/qAyXFx6iJ3C3XyuMnMbJdDI9ee0spAvFz7cQ== + +"@tiptap/extension-code-block-lowlight@^2.1.11": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.1.12.tgz#ccbca5d0d92bee373dc8e2e2ae6c27f62f66437c" + integrity sha512-dtIbpI9QrWa9TzNO4v5q/zf7+d83wpy5i9PEccdJAVtRZ0yOI8JIZAWzG5ex3zAoCA0CnQFdsPSVykYSDdxtDA== + +"@tiptap/extension-code-block@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.1.12.tgz#20416baef1b5fc839490a8416e97fdcbb5fdf918" + integrity sha512-RXtSYCVsnk8D+K80uNZShClfZjvv1EgO42JlXLVGWQdIgaNyuOv/6I/Jdf+ZzhnpsBnHufW+6TJjwP5vJPSPHA== + +"@tiptap/extension-code@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.1.12.tgz#86d2eb5f63725af472c5fd858e5a9c7ccae06ef3" + integrity sha512-CRiRq5OTC1lFgSx6IMrECqmtb93a0ZZKujEnaRhzWliPBjLIi66va05f/P1vnV6/tHaC3yfXys6dxB5A4J8jxw== + +"@tiptap/extension-color@^2.1.11": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-color/-/extension-color-2.1.12.tgz#1076833f061c3eabf59aef32cd15f103fd6ec710" + integrity sha512-Myd6iSbPJvvclr+NRBEdE0k52QlQrXZnJljk4JKn0b25cl60ERA40FH9QLBjkpTed7SDbI3oX7LWIzTUoCj39w== + +"@tiptap/extension-document@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.1.12.tgz#e19e4716dfad60cbeb6abaf2f362fed759963529" + integrity sha512-0QNfAkCcFlB9O8cUNSwTSIQMV9TmoEhfEaLz/GvbjwEq4skXK3bU+OQX7Ih07waCDVXIGAZ7YAZogbvrn/WbOw== + +"@tiptap/extension-dropcursor@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.1.12.tgz#9da0c275291c9d47497d3db41b4d70d96366b4ff" + integrity sha512-0tT/q8nL4NBCYPxr9T0Brck+RQbWuczm9nV0bnxgt0IiQXoRHutfPWdS7GA65PTuVRBS/3LOco30fbjFhkfz/A== + +"@tiptap/extension-floating-menu@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.1.12.tgz#68a658b2b9bdd3a0fc1afc5165231838061a8fde" + integrity sha512-uo0ydCJNg6AWwLT6cMUJYVChfvw2PY9ZfvKRhh9YJlGfM02jS4RUG/bJBts6R37f+a5FsOvAVwg8EvqPlNND1A== dependencies: tippy.js "^6.3.7" -"@tiptap/extension-gapcursor@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.4.tgz#c100a792fd41535ad6382aa8133d0d9c0b2cb2b8" - integrity sha512-VxmKfBQjSSu1mNvHlydA4dJW/zawGKyqmnryiFNcUV9s+/HWLR5i9SiUl4wJM/B8sG8cQxClne5/LrCAeGNYuA== - -"@tiptap/extension-hard-break@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.4.tgz#a4f70fa9a473270f7ec89f20a14b9122af5657bc" - integrity sha512-4j8BZa6diuoRytWoIc7j25EYWWut5TZDLbb+OVURdkHnsF8B8zeNTo55W40CdwSaSyTtXtxbTIldV80ShQarGQ== - -"@tiptap/extension-heading@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.0.4.tgz#5372e346c5d69cfa0060d7238d1a0bf440442f6f" - integrity sha512-EfitUbew5ljH3xVlBXAxqqcJ4rjv15b8379LYOV6KQCf+Y1wY0gy9Q8wXSnrsAagqrvqipja4Ihn3OZeyIM+CA== - -"@tiptap/extension-highlight@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.0.4.tgz#5d54232ac573d0b04c3e705ca38f207fb6cf7270" - integrity sha512-z1hcpf0eHHdaBE0pewXiNIu+QBodw4IAbZykTXMaY1xCsbYWfOJxeIb5o+CEG5HBsmaoJrCYenQw71xzgV0hKA== - -"@tiptap/extension-history@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.4.tgz#761a9c4b2a875817acc73137660552bd49e94fca" - integrity sha512-3GAUszn1xZx3vniHMiX9BSKmfvb5QOb0oSLXInN+hx80CgJDIHqIFuhx2dyV9I/HWpa0cTxaLWj64kfDzb1JVg== - -"@tiptap/extension-horizontal-rule@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.4.tgz#6988dd63fb00ca144feb1baac84142782e8ebe38" - integrity sha512-OMx2ImQseKbSUjPbbRCuYGOJshxYedh9giWAqwgWWokhYkH4nGxXn5m7+Laj+1wLre4bnWgHWVY4wMGniEj3aw== - -"@tiptap/extension-image@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.4.tgz#a41d5ca246bd41dd293194e359a80cb97f477ab3" - integrity sha512-5iQ96pt9xppM8sWzwhGgc99PPoYPQuokTaCXAQKDI0Y1CFCjZ+/duUG3al1VUMpBXsjJw3/RVO1+7CEhRTd3mA== - -"@tiptap/extension-italic@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.4.tgz#4c6d0938542e4f7276f9dd18db395c040f76dcd8" - integrity sha512-C/6+qs4Jh8xERRP0wcOopA1+emK8MOkBE4RQx5NbPnT2iCpERP0GlmHBFQIjaYPctZgKFHxsCfRnneS5Xe76+A== - -"@tiptap/extension-link@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.4.tgz#2899f9060ca722f11bd10ceb572ceb5178f111d6" - integrity sha512-CliImI1hmC+J6wHxqgz9P4wMjoNSSgm3fnNHsx5z0Bn6JRA4Evh2E3KZAdMaE8xCTx89rKxMYNbamZf4VLSoqQ== +"@tiptap/extension-gapcursor@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.1.12.tgz#63844c3abd1a38af915839cf0c097b6d2e5a86fe" + integrity sha512-zFYdZCqPgpwoB7whyuwpc8EYLYjUE5QYKb8vICvc+FraBUDM51ujYhFSgJC3rhs8EjI+8GcK8ShLbSMIn49YOQ== + +"@tiptap/extension-hard-break@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.1.12.tgz#54d0c9996e1173594852394975a9356eec98bc9a" + integrity sha512-nqKcAYGEOafg9D+2cy1E4gHNGuL12LerVa0eS2SQOb+PT8vSel9OTKU1RyZldsWSQJ5rq/w4uIjmLnrSR2w6Yw== + +"@tiptap/extension-heading@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.1.12.tgz#05ae4684d6f29ae611495ab114038e14a5d1dff6" + integrity sha512-MoANP3POAP68Ko9YXarfDKLM/kXtscgp6m+xRagPAghRNujVY88nK1qBMZ3JdvTVN6b/ATJhp8UdrZX96TLV2w== + +"@tiptap/extension-history@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.1.12.tgz#03bcb9422e8ea2b82dc45207d1a1b0bc0241b055" + integrity sha512-6b7UFVkvPjq3LVoCTrYZAczt5sQrQUaoDWAieVClVZoFLfjga2Fwjcfgcie8IjdPt8YO2hG/sar/c07i9vM0Sg== + +"@tiptap/extension-horizontal-rule@^2.1.11", "@tiptap/extension-horizontal-rule@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.1.12.tgz#2191d4ff68ed39381d65971ad8e2aa1be43e6d6b" + integrity sha512-RRuoK4KxrXRrZNAjJW5rpaxjiP0FJIaqpi7nFbAua2oHXgsCsG8qbW2Y0WkbIoS8AJsvLZ3fNGsQ8gpdliuq3A== + +"@tiptap/extension-image@^2.1.7": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.1.12.tgz#ab035db82f0961b1d906c4d426bf68be563fdcd3" + integrity sha512-VCgOTeNLuoR89WoCESLverpdZpPamOd7IprQbDIeG14sUySt7RHNgf2AEfyTYJEHij12rduvAwFzerPldVAIJg== + +"@tiptap/extension-italic@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.1.12.tgz#e99480eb77f8b4e5444fc236add8a831d5aa2343" + integrity sha512-/XYrW4ZEWyqDvnXVKbgTXItpJOp2ycswk+fJ3vuexyolO6NSs0UuYC6X4f+FbHYL5VuWqVBv7EavGa+tB6sl3A== + +"@tiptap/extension-link@^2.1.7": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.1.12.tgz#a18f83a0b54342e6274ff9e5a5907ef7f15aa723" + integrity sha512-Sti5hhlkCqi5vzdQjU/gbmr8kb578p+u0J4kWS+SSz3BknNThEm/7Id67qdjBTOQbwuN07lHjDaabJL0hSkzGQ== dependencies: linkifyjs "^4.1.0" -"@tiptap/extension-list-item@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.4.tgz#8ca7c9959a07bf94602f8957d784d526568f2069" - integrity sha512-tSkbLgRo1QMNDJttWs9FeRywkuy5T2HdLKKfUcUNzT3s0q5AqIJl7VyimsBL4A6MUfN1qQMZCMHB4pM9Mkluww== - -"@tiptap/extension-ordered-list@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.4.tgz#e3e220e9c15114b07c952c32fa58e96601db6bd7" - integrity sha512-Kfg+8k9p4iJCUKP/yIa18LfUpl9trURSMP/HX3/yQTz9Ul1vDrjxeFjSE5uWNvupcXRAM24js+aYrCmV7zpU+Q== - -"@tiptap/extension-paragraph@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.4.tgz#9cffa3f8a980349ca068b1b3c12596bf5f3aef0f" - integrity sha512-nDxpopi9WigVqpfi8nU3B0fWYB14EMvKIkutNZo8wJvKGTZufNI8hw66wupIx/jZH1gFxEa5dHerw6aSYuWjgQ== - -"@tiptap/extension-placeholder@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.4.tgz#74259783757c59751d78fcdd1aade7e928809187" - integrity sha512-Y8hjUYBGTbytgrsplSZdHGciqbuVHQX+h0JcuvVaIlAy1kR7hmbxJLqL8tNa7qLtTqo2MfS2942OtSv85JOCzA== - -"@tiptap/extension-strike@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.4.tgz#13286dcf8780c55610ed65b24238b8395a5be824" - integrity sha512-Men7LK6N/Dh3/G4/z2Z9WkDHM2Gxx1XyxYix2ZMf5CnqY37SeDNUnGDqit65pdIN3Y/TQnOZTkKSBilSAtXfJA== - -"@tiptap/extension-task-item@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.0.4.tgz#71f46d35ac629ca10c5c23d4ad170007338a436e" - integrity sha512-0FfYWrOslDzzN7Ehnt3yBekOSH45tiB/3gzFRvGdLBUv0PiYQolUpyfHGsdNzeKYuWLF1yiacJkCeLgNDgCLDw== - -"@tiptap/extension-task-list@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.0.4.tgz#69b2b23d1e757044c05f3d7dcbd30194c71f9324" - integrity sha512-3RGoEgGJdWpGf8aWl7O7+jnnvfpF0or2YHYYvJv13t5G4dNIS9E7QXT3/rU9QtHNYkbcJYFjHligIFuBTAhZNg== - -"@tiptap/extension-text-style@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text-style/-/extension-text-style-2.0.4.tgz#4ba3fd6b204badc43ac6a00285315fe868f07e52" - integrity sha512-HQk8c7HasDdeAJxlHrztkgprxocZecZVUMlvPvFAhkq8E/5+nfmr/Gm9qudiStEARZrIYBATNA2PbnQuIGMx3A== - -"@tiptap/extension-text@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.4.tgz#318b0105491a5976d220871dccabe6c4d2cbeedd" - integrity sha512-i8/VFlVZh7TkAI49KKX5JmC0tM8RGwyg5zUpozxYbLdCOv07AkJt+E1fLJty9mqH4Y5HJMNnyNxsuZ9Ol/ySRA== - -"@tiptap/extension-underline@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-2.0.4.tgz#c1e5df75a4c9f2d9e691d48438ee0894f8bb01f1" - integrity sha512-Hvhy3iV5dWs0SFTww6sIzyQSSgVzcQuiozhDs11iP+gvFjK7ejg86KZ8wAVvyCi9K3bOMhohsw1Q2b8JSnIxcg== - -"@tiptap/pm@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.0.4.tgz#c3df31a29120e1e3334f8f063df23ccb1ace7851" - integrity sha512-DNgxntpEaiW7ciW0BTNTL0TFqAreZTrAROWakI4XaYRAyi5H9NfZW8jmwGwMBkoZ1KB3pfy+jT/Bisy4okEQGQ== +"@tiptap/extension-list-item@^2.1.11", "@tiptap/extension-list-item@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.12.tgz#3eb28dc998490a98f14765783770b3cf6587d39e" + integrity sha512-Gk7hBFofAPmNQ8+uw8w5QSsZOMEGf7KQXJnx5B022YAUJTYYxO3jYVuzp34Drk9p+zNNIcXD4kc7ff5+nFOTrg== + +"@tiptap/extension-mention@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.1.12.tgz#a395e7757b45630ec3047f14b0ba2dde8e1c9c93" + integrity sha512-Nc8wFlyPp+/48IpOFPk2O3hYsF465wizcM3aihMvZM96Ahic7dvv9yVptyOfoOwgpExl2FIn1QPjRDXF60VAUg== + +"@tiptap/extension-ordered-list@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.12.tgz#f41a45bc66b4d19e379d4833f303f2e0cd6b9d60" + integrity sha512-tF6VGl+D2avCgn9U/2YLJ8qVmV6sPE/iEzVAFZuOSe6L0Pj7SQw4K6AO640QBob/d8VrqqJFHCb6l10amJOnXA== + +"@tiptap/extension-paragraph@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.1.12.tgz#922447b2aa1c7184787d351ceec593a74d24ed03" + integrity sha512-hoH/uWPX+KKnNAZagudlsrr4Xu57nusGekkJWBcrb5MCDE91BS+DN2xifuhwXiTHxnwOMVFjluc0bPzQbkArsw== + +"@tiptap/extension-placeholder@^2.1.11": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.1.12.tgz#f6267a563d17a5ae8a04da32231eac8d8868519e" + integrity sha512-K52o7B1zkP4vaVy3z4ZwHn+tQy6KlXtedj1skLg+796ImwH2GYS5z6MFOTfKzBO2hLncUzLco/s0C5PLCD6SDw== + +"@tiptap/extension-strike@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.1.12.tgz#2b049aedf2985e9c9e3c3f1cc0b203a574c85bd8" + integrity sha512-HlhrzIjYUT8oCH9nYzEL2QTTn8d1ECnVhKvzAe6x41xk31PjLMHTUy8aYjeQEkWZOWZ34tiTmslV1ce6R3Dt8g== + +"@tiptap/extension-table-cell@^2.1.6": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.1.12.tgz#b13938d345065a3750610c66a81ea107edbbcea7" + integrity sha512-hextcfVTdwX8G7s8Q/V6LW2aUhGvPgu1dfV+kVVO42AFHxG+6PIkDOUuHphGajG3Nrs129bjMDWb8jphj38dUg== + +"@tiptap/extension-table-header@^2.1.6": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.1.12.tgz#87ac2efa073a212c6114e0b137cf4afc3d75c35f" + integrity sha512-a4WZ5Z7gqQ/QlK8cK2d1ONYdma/J5+yH/0SNtQhkfELoS45GsLJh89OyKO0W0FnY6Mg0RoH1FsoBD+cqm0yazA== + +"@tiptap/extension-table-row@^2.1.6": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.1.12.tgz#27bee7d046b2bea4fe6bf46260e0d89305b75663" + integrity sha512-0kPr+zngQC1YQRcU6+Fl3CpIW/SdJhVJ5qOLpQleXrLPdjmZQd3Z1DXvOSDphYjXCowGPCxeUa++6bo7IoEMJw== + +"@tiptap/extension-table@^2.1.6": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.1.12.tgz#173cc4eac75c650b440dfcae433d3c74e78aa1bc" + integrity sha512-q/DuKZ4j1ycRfuFdb9rBJ3MglGNxlM2BQ1csScX/BrVIsAQI5B8sdzy1BrIlepQ6DRu4DCzHcKMI8u4/edUSWA== + +"@tiptap/extension-task-item@^2.1.7": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.1.12.tgz#944eacf6f0ed1a430d807217d62b49ccef3956e1" + integrity sha512-uqrDTO4JwukZUt40GQdvB6S+oDhdp4cKNPMi0sbteWziQugkSMLlkYvxU0Hfb/YeziaWWwFI7ssPu/hahyk6dQ== + +"@tiptap/extension-task-list@^2.1.7": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.1.12.tgz#adbfb5a5b990d6f189c776b45de2d2c5bb77e963" + integrity sha512-BUpYlEWK+Q3kw9KIiOqvhd0tUPhMcOf1+fJmCkluJok+okAxMbP1umAtCEQ3QkoCwLr+vpHJov7h3yi9+dwgeQ== + +"@tiptap/extension-text-style@^2.1.11": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text-style/-/extension-text-style-2.1.12.tgz#ec6a025fc6785246c9fcf78e34088759c1d2b213" + integrity sha512-nfjWXX0JSRHLcscfiMESh+RN+Z7bG8nio/C9+8yQASM90VxU9f8oKgF8HnnSYsSrD4lLf44Q6XjmB7aMVUuikg== + +"@tiptap/extension-text@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.1.12.tgz#466e3244bdd9b2db2304c0c9a1d51ce59f5327d0" + integrity sha512-rCNUd505p/PXwU9Jgxo4ZJv4A3cIBAyAqlx/dtcY6cjztCQuXJhuQILPhjGhBTOLEEL4kW2wQtqzCmb7O8i2jg== + +"@tiptap/extension-underline@^2.1.7": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-2.1.12.tgz#abd59c4b6c8434dbadb4ff9bff23eefcc6bc095e" + integrity sha512-NwwdhFT8gDD0VUNLQx85yFBhP9a8qg8GPuxlGzAP/lPTV8Ubh3vSeQ5N9k2ZF/vHlEvnugzeVCbmYn7wf8vn1g== + +"@tiptap/pm@^2.1.7": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.1.12.tgz#88a4b19be0eabb13d42ddd540c19ba1bbe74b322" + integrity sha512-Q3MXXQABG4CZBesSp82yV84uhJh/W0Gag6KPm2HRWPimSFELM09Z9/5WK9RItAYE0aLhe4Krnyiczn9AAa1tQQ== dependencies: prosemirror-changeset "^2.2.0" prosemirror-collab "^1.3.0" @@ -2398,60 +2554,65 @@ prosemirror-transform "^1.7.0" prosemirror-view "^1.28.2" -"@tiptap/react@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.4.tgz#b879faeabd67859254d594eafe0f8232f5d78116" - integrity sha512-NcrZL4Tu3+1Xfj/us5AOD7+kJhwYo2XViOB2iRRnfwS80PUtiLWDis6o3ngMGot/jBWzaMn4gofXnMWHtFdIAw== - dependencies: - "@tiptap/extension-bubble-menu" "^2.0.4" - "@tiptap/extension-floating-menu" "^2.0.4" - -"@tiptap/starter-kit@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.0.4.tgz#20456eb4a4ae0ac8d5bf2ac5e9771b3c617c51a6" - integrity sha512-9WtVXhujyp5cOlE7qlcQMFr0FEx3Cvo1isvfQGzhKKPzXa3rR7FT8bnOFsten31/Ia/uwvGXAvRDQy24YfHdNA== - dependencies: - "@tiptap/core" "^2.0.4" - "@tiptap/extension-blockquote" "^2.0.4" - "@tiptap/extension-bold" "^2.0.4" - "@tiptap/extension-bullet-list" "^2.0.4" - "@tiptap/extension-code" "^2.0.4" - "@tiptap/extension-code-block" "^2.0.4" - "@tiptap/extension-document" "^2.0.4" - "@tiptap/extension-dropcursor" "^2.0.4" - "@tiptap/extension-gapcursor" "^2.0.4" - "@tiptap/extension-hard-break" "^2.0.4" - "@tiptap/extension-heading" "^2.0.4" - "@tiptap/extension-history" "^2.0.4" - "@tiptap/extension-horizontal-rule" "^2.0.4" - "@tiptap/extension-italic" "^2.0.4" - "@tiptap/extension-list-item" "^2.0.4" - "@tiptap/extension-ordered-list" "^2.0.4" - "@tiptap/extension-paragraph" "^2.0.4" - "@tiptap/extension-strike" "^2.0.4" - "@tiptap/extension-text" "^2.0.4" - -"@tiptap/suggestion@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.4.tgz#08e6c47a723200d02238d845cb09684c481f0066" - integrity sha512-C5LGGjH8VFET34V7vKkqlwpSzrPl+7oAcj9h+P3jvJQ076iYpmpnMtz6dNLSFGKpHp5mtyl4RoJzh7lTvlfyiA== +"@tiptap/prosemirror-tables@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@tiptap/prosemirror-tables/-/prosemirror-tables-1.1.4.tgz#e123978f13c9b5f980066ba660ec5df857755916" + integrity sha512-O2XnDhZV7xTHSFxMMl8Ei3UVeCxuMlbGYZ+J2QG8CzkK8mxDpBa66kFr5DdyAhvdi1ptpcH9u7/GMwItQpN4sA== + +"@tiptap/react@^2.1.7": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.1.12.tgz#23566c7992b9642137171b282335e646922ae559" + integrity sha512-RMO4QmmpL7sPR7w8o1Wq0hrUe/ttHzsn5I/eWwqg1d3fGx5y9mOdfCoQ9XBtm49Xzdejy3QVzt4zYp9fX0X/xg== + dependencies: + "@tiptap/extension-bubble-menu" "^2.1.12" + "@tiptap/extension-floating-menu" "^2.1.12" + +"@tiptap/starter-kit@^2.1.10": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.1.12.tgz#2bf28091ed08dc8f7b903ba92925e4ffe06257ea" + integrity sha512-+RoP1rWV7rSCit2+3wl2bjvSRiePRJE/7YNKbvH8Faz/+AMO23AFegHoUFynR7U0ouGgYDljGkkj35e0asbSDA== + dependencies: + "@tiptap/core" "^2.1.12" + "@tiptap/extension-blockquote" "^2.1.12" + "@tiptap/extension-bold" "^2.1.12" + "@tiptap/extension-bullet-list" "^2.1.12" + "@tiptap/extension-code" "^2.1.12" + "@tiptap/extension-code-block" "^2.1.12" + "@tiptap/extension-document" "^2.1.12" + "@tiptap/extension-dropcursor" "^2.1.12" + "@tiptap/extension-gapcursor" "^2.1.12" + "@tiptap/extension-hard-break" "^2.1.12" + "@tiptap/extension-heading" "^2.1.12" + "@tiptap/extension-history" "^2.1.12" + "@tiptap/extension-horizontal-rule" "^2.1.12" + "@tiptap/extension-italic" "^2.1.12" + "@tiptap/extension-list-item" "^2.1.12" + "@tiptap/extension-ordered-list" "^2.1.12" + "@tiptap/extension-paragraph" "^2.1.12" + "@tiptap/extension-strike" "^2.1.12" + "@tiptap/extension-text" "^2.1.12" + +"@tiptap/suggestion@^2.0.4", "@tiptap/suggestion@^2.1.7": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.1.12.tgz#a13782d1e625ec03b3f61b6839ecc95b6b685d3f" + integrity sha512-rhlLWwVkOodBGRMK0mAmE34l2a+BqM2Y7q1ViuQRBhs/6sZ8d83O4hARHKVwqT5stY4i1l7d7PoemV3uAGI6+g== "@types/debug@^4.0.0": - version "4.1.8" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" - integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ== + version "4.1.10" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.10.tgz#f23148a6eb771a34c466a4fc28379d8101e84494" + integrity sha512-tOSCru6s732pofZ+sMv9o4o3Zc+Sa8l3bxd/tweTQudFn06vAzb13ZX46Zi6m6EJ+RUbRTHvgQJ1gBtSgkaUYA== dependencies: "@types/ms" "*" "@types/dom4@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.2.tgz#6495303f049689ce936ed328a3e5ede9c51408ee" - integrity sha512-Rt4IC1T7xkCWa0OG1oSsPa0iqnxlDeQqKXZAHrQGLb7wFGncWm85MaxKUjAGejOrUynOgWlFi4c6S6IyJwoK4g== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.3.tgz#bd084dbd4c15bee49442c5cd231acdcd14efbe90" + integrity sha512-xQT2XxtDGP1WFfTB/Lti629HpguNrfZ3dg84bWXASd6JUay6WgR73Wb6DG3kmr2/iGAWZ7NNLceGVWYWfgPX0g== "@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.4.tgz#d9748f5742171b26218516cf1828b8eafaf8a9fa" + integrity sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw== "@types/estree@0.0.39": version "0.0.39" @@ -2467,29 +2628,36 @@ "@types/node" "*" "@types/hast@^2.0.0": - version "2.3.5" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.5.tgz#08caac88b44d0fdd04dc17a19142355f43bd8a7a" - integrity sha512-SvQi0L/lNpThgPoleH53cdjB3y9zpLlVjRbqB3rH8hx1jiRSBGAhyjV3H+URFjNVRqt2EdYNrbZE5IsGlNfpRg== + version "2.3.7" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.7.tgz#5e9bd7ab4452d5313aeec9d38fbc193a70f8d810" + integrity sha512-EVLigw5zInURhzfXUM65eixfadfsHKomGKUakToXo84t8gGIJuTcD2xooM2See7GyQ7DRtYjhCHnSUQez8JaLw== dependencies: "@types/unist" "^2" -"@types/hoist-non-react-statics@^3.3.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== +"@types/hast@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.2.tgz#e6c1126a33955cb9493a5074ddf1873fb48248c7" + integrity sha512-B5hZHgHsXvfCoO3xgNJvBnX7N8p86TqQeGKXcokW4XXi+qY4vxxPSFYofytvVmpFxzPv7oxDQzjg5Un5m2/xiw== + dependencies: + "@types/unist" "*" + +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.4" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.4.tgz#cc477ce0283bb9d19ea0cbfa2941fe2c8493a1be" + integrity sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ== dependencies: "@types/react" "*" hoist-non-react-statics "^3.3.0" "@types/js-cookie@^3.0.2", "@types/js-cookie@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.3.tgz#d6bfbbdd0c187354ca555213d1962f6d0691ff4e" - integrity sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.5.tgz#5eba4033a4f17fb2b29d975892694315194eca33" + integrity sha512-dtLshqoiGRDHbHueIT9sjkd2F4tW1qPSX2xKAQK8p1e6pM+Z913GM1shv7dOqqasEMYbC5zEaClJomQe8OtQLA== "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.12" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" - integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + version "7.0.14" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.14.tgz#74a97a5573980802f32c8e47b663530ab3b6b7d1" + integrity sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw== "@types/json5@^0.0.29": version "0.0.29" @@ -2497,21 +2665,21 @@ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/linkify-it@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" - integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.4.tgz#def6a9bb0ce78140860602f16ace37a9997f086a" + integrity sha512-hPpIeeHb/2UuCw06kSNAOVWgehBLXEo0/fUs0mw3W2qhqX89PI2yvok83MnuctYGCPrabGIoi0fFso4DQ+sNUQ== "@types/lodash.debounce@^4.0.7": - version "4.0.7" - resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f" - integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.8.tgz#d5fe36a35aa57773e05d960b3e3c703fd9ffb8b3" + integrity sha512-REumepIJjQFSOaBUoj81U5ZzF9YIhovzE2Lm6ejUbycmwx597k2ivG1cVfPtAj4eVuSbGoZDkJR0sRIahsE6/Q== dependencies: "@types/lodash" "*" "@types/lodash@*": - version "4.14.197" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.197.tgz#e95c5ddcc814ec3e84c891910a01e0c8a378c54b" - integrity sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g== + version "4.14.200" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149" + integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q== "@types/markdown-it@^12.2.3": version "12.2.3" @@ -2522,16 +2690,16 @@ "@types/mdurl" "*" "@types/mdast@^3.0.0": - version "3.0.12" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.12.tgz#beeb511b977c875a5b0cc92eab6fcac2f0895514" - integrity sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg== + version "3.0.14" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.14.tgz#0735473a5b35be032b9f2685b7413cbab1b8a639" + integrity sha512-gVZ04PGgw1qLZKsnWnyFv4ORnaJ+DXLdHTVSFbU8yX6xZ34Bjg4Q32yPkmveUP1yItXReKfB0Aknlh/3zxTKAw== dependencies: "@types/unist" "^2" "@types/mdurl@*": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" - integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.4.tgz#574bfbec51eb41ab5f444116c8555bc4347feba5" + integrity sha512-ARVxjAEX5TARFRzpDRVC6cEk0hUIXCCwaMhz8y7S1/PxU6zZS1UMjyobz7q4w/D/R552r4++EhwmXK1N2rAy0A== "@types/minimatch@*": version "5.1.2" @@ -2539,14 +2707,16 @@ integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== "@types/ms@*": - version "0.7.31" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" - integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + version "0.7.33" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.33.tgz#80bf1da64b15f21fd8c1dc387c31929317d99ee9" + integrity sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ== -"@types/node@*": - version "20.5.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.0.tgz#7fc8636d5f1aaa3b21e6245e97d56b7f56702313" - integrity sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q== +"@types/node@*", "@types/node@^20.5.2": + version "20.8.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.10.tgz#a5448b895c753ae929c26ce85cab557c6d4a365e" + integrity sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w== + dependencies: + undici-types "~5.26.4" "@types/node@18.0.6": version "18.0.6" @@ -2558,50 +2728,48 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.1.tgz#90dad8476f1e42797c49d6f8b69aaf9f876fc69f" integrity sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ== +"@types/node@18.15.3": + version "18.15.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014" + integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw== + "@types/nprogress@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f" - integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A== + version "0.2.2" + resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.2.tgz#c73bf540ac7926fb1b6d03f9d2725e07b3848d65" + integrity sha512-2wLrSJXLztGmr7wXwM0hA/wuIOY9DznVdd+ZFofHOiXcj9JnVt+2ZeLRJ7v5ZVlmheSkUOSg3Q3O4Ce7yji79A== "@types/object.omit@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/object.omit/-/object.omit-3.0.0.tgz#0d31e1208eac8fe2ad5c9499a1016a8273bbfafc" - integrity sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/object.omit/-/object.omit-3.0.2.tgz#13d23915cc16fa54b0d4cfbcb79840f4fe1474d9" + integrity sha512-BxWU36cMP+FKD3OLFluQaj2cBev2sx2LJaHELuphHwnleq+xnEhTmuYYYx4pOT/1U/ZoR6B+RdvxWh2FD6lGGA== "@types/object.pick@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/object.pick/-/object.pick-1.3.2.tgz#9eb28118240ad8f658b9c9c6caf35359fdb37150" - integrity sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg== + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/object.pick/-/object.pick-1.3.3.tgz#f4d4a76e9ef1161e965b963d2bb33c3f6c300125" + integrity sha512-qZqHmdGEALeSATMB1djT1S5szv6Wtpb7DKpHrt2XG4iyKlV7C2Xk8GmDXr1KXakOqUfX6ohw7ceruYt4NVmB1Q== "@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.1.tgz#27f7559836ad796cea31acb63163b203756a5b4e" + integrity sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng== -"@types/prop-types@*", "@types/prop-types@^15.0.0", "@types/prop-types@^15.7.5": - version "15.7.5" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== +"@types/prop-types@*", "@types/prop-types@^15.0.0", "@types/prop-types@^15.7.9": + version "15.7.9" + 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.4" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.4.tgz#bcec72da719c18c0d8b4a7cb00e7fb443211d6d7" - integrity sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA== - dependencies: - "@types/react" "*" - -"@types/react-color@^3.0.6": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.6.tgz#602fed023802b2424e7cd6ff3594ccd3d5055f9a" - integrity sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w== +"@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" + integrity sha512-Ojyc6jySSKvM6UYQrZxaYe0JZXtgHHXwR2q9H4MhcNCswFdeZH1owYZCvPtdHtMOfh7t8h1fY0Gd0nvU1JGDkQ== dependencies: "@types/react" "*" "@types/reactcss" "*" "@types/react-datepicker@^4.8.0": - version "4.15.0" - resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.15.0.tgz#24a9c03e79ab4b232b346edd006cfb6060b0fb43" - integrity sha512-kr10s8ex4+MmCJmzdhA9kfmoMQBmsW5uDYDlH+8f/PgStrp7rRaz23Y/cvTiMgvESVq8ujDh4SOo6jlSwEw13g== + version "4.19.1" + resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.19.1.tgz#c7102c015eb0f63b0f1707e34799d289408654be" + integrity sha512-HV52yjxuRi49psAVuHTFXXr+RSrCbIFDn9ayei0YH8xVVAXCO+5GwHAGKeREAmNbneweN0ySGoByr90yJCAnrQ== dependencies: "@popperjs/core" "^2.9.2" "@types/react" "*" @@ -2615,75 +2783,40 @@ 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.0.6": - version "18.2.7" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63" - integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA== - dependencies: - "@types/react" "*" - -"@types/react-is@^18.2.1": - version "18.2.1" - resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-18.2.1.tgz#61d01c2a6fc089a53520c0b66996d458fdc46863" - integrity sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw== +"@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" + integrity sha512-8yQrvS6sMpSwIovhPOwfyNf2Wz6v/B62LFSVYQ85+Rq3tLsBIG7rP5geMxaijTUxSkrO6RzN/IRuIAADYQsleA== dependencies: "@types/react" "*" -"@types/react-redux@^7.1.20": - version "7.1.25" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88" - integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg== +"@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/hoist-non-react-statics" "^3.3.0" "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" -"@types/react-transition-group@^4.4.6": - version "4.4.6" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.6.tgz#18187bcda5281f8e10dfc48f0943e2fdf4f75e2e" - integrity sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew== +"@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" + integrity sha512-QmQ22q+Pb+HQSn04NL3HtrqHwYMf4h3QKArOy5F8U5nEVMaihBs3SR10WiOM1iwPz5jIo8x/u11al+iEGZZrvg== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.0.17": - version "18.2.20" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.20.tgz#1605557a83df5c8a2cc4eeb743b3dfc0eb6aaeb2" - integrity sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@18.0.15": - version "18.0.15" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.15.tgz#d355644c26832dc27f3e6cbf0c4f4603fc4ab7fe" - integrity sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@18.0.28": - version "18.0.28" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" - integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== +"@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== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" csstype "^3.0.2" "@types/reactcss@*": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.6.tgz#133c1e7e896f2726370d1d5a26bf06a30a038bcc" - integrity sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg== + version "1.2.8" + resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.8.tgz#8c2342f5f650cc5f9e8bea73199c17ba747b9126" + integrity sha512-IzxChTOxOFWZb1RhXoNZ7oEi3BtUdLQIFheoOurvu6iu0X9kwhoFe73DW9EVFxVFTKnd8bb8b1dKtO0tokM3eA== dependencies: "@types/react" "*" @@ -2695,14 +2828,14 @@ "@types/node" "*" "@types/scheduler@*": - version "0.16.3" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" - integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== + version "0.16.5" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.5.tgz#4751153abbf8d6199babb345a52e1eb4167d64af" + integrity sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw== "@types/semver@^7.3.12": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" - integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== + version "7.5.4" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.4.tgz#0a41252ad431c473158b22f9bfb9a63df7541cff" + integrity sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ== "@types/throttle-debounce@^2.1.0": version "2.1.0" @@ -2710,14 +2843,24 @@ integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ== "@types/trusted-types@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311" - integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g== + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.5.tgz#5cac7e7df3275bb95f79594f192d97da3b4fd5fe" + integrity sha512-I3pkr8j/6tmQtKV/ZzHtuaqYSQvyjGRKH4go60Rr0IDLlFxuRT5V32uvB1mecM5G1EVAUyF/4r4QZ1GHgz+mxA== + +"@types/unist@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.1.tgz#778652d02ddec1bfc9e5e938fec8d407b8e56cba" + integrity sha512-ue/hDUpPjC85m+PM9OQDMZr3LywT+CT6mPsQq8OJtCLiERkGRcQUFvu9XASF5XWqyZFXbf15lvb3JFJ4dRLWPg== "@types/unist@^2", "@types/unist@^2.0.0": - version "2.0.7" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.7.tgz#5b06ad6894b236a1d2bd6b2f07850ca5c59cf4d6" - integrity sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g== + version "2.0.9" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.9.tgz#72e164381659a49557b0a078b28308f2c6a3e1ce" + integrity sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ== + +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== "@types/uuid@^8.3.4": version "8.3.4" @@ -2725,11 +2868,11 @@ integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== "@types/uuid@^9.0.1": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b" - integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ== + version "9.0.6" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.6.tgz#c91ae743d8344a54b2b0c691195f5ff5265f6dfb" + integrity sha512-BT2Krtx4xaO6iwzwMFUYvWBWkV2pr37zD68Vmp1CDV196MzczBRxuEpD6Pr395HAgebC/co7hOphs53r8V7jew== -"@typescript-eslint/eslint-plugin@^5.48.2", "@typescript-eslint/eslint-plugin@^5.51.0": +"@typescript-eslint/eslint-plugin@^5.48.2": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== @@ -2813,6 +2956,11 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -2824,9 +2972,9 @@ acorn@^7.4.0: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.8.2, acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== agent-base@6: version "6.0.2" @@ -2921,7 +3069,7 @@ aria-hidden@^1.1.1: dependencies: tslib "^2.0.0" -aria-query@^5.1.3: +aria-query@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== @@ -2936,15 +3084,15 @@ array-buffer-byte-length@^1.0.0: call-bind "^1.0.2" is-array-buffer "^3.0.1" -array-includes@^3.1.5, array-includes@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" - integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== +array-includes@^3.1.5, array-includes@^3.1.6, array-includes@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" + integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - get-intrinsic "^1.1.3" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" is-string "^1.0.7" array-union@^1.0.1: @@ -2964,64 +3112,65 @@ array-uniq@^1.0.1: resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== -array.prototype.findlastindex@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz#bc229aef98f6bd0533a2bc61ff95209875526c9b" - integrity sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw== +array.prototype.findlastindex@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207" + integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" - get-intrinsic "^1.1.3" + get-intrinsic "^1.2.1" -array.prototype.flat@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" - integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.0, array.prototype.flatmap@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" - integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== +array.prototype.flatmap@^1.3.0, array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" array.prototype.tosorted@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" - integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz#620eff7442503d66c799d95503f82b475745cefd" + integrity sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" - get-intrinsic "^1.1.3" + get-intrinsic "^1.2.1" -arraybuffer.prototype.slice@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb" - integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw== +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== dependencies: array-buffer-byte-length "^1.0.0" call-bind "^1.0.2" define-properties "^1.2.0" + es-abstract "^1.22.1" get-intrinsic "^1.2.1" is-array-buffer "^3.0.2" is-shared-array-buffer "^1.0.2" -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== astral-regex@^2.0.0: version "2.0.0" @@ -3029,9 +3178,16 @@ 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" + resolved "https://registry.yarnpkg.com/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz#8c5df0514936cdd133604dfcc9d3fb93f09b2b62" + integrity sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg== + dependencies: + has-symbols "^1.0.3" asynckit@^0.4.0: version "0.4.0" @@ -3048,14 +3204,14 @@ attr-accept@^2.2.2: resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== -autoprefixer@^10.4.13, autoprefixer@^10.4.7: - version "10.4.15" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.15.tgz#a1230f4aeb3636b89120b34a1f513e2f6834d530" - integrity sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew== +autoprefixer@^10.4.14, autoprefixer@^10.4.15: + version "10.4.16" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8" + integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ== dependencies: browserslist "^4.21.10" - caniuse-lite "^1.0.30001520" - fraction.js "^4.2.0" + caniuse-lite "^1.0.30001538" + fraction.js "^4.3.6" normalize-range "^0.1.2" picocolors "^1.0.0" postcss-value-parser "^4.2.0" @@ -3065,21 +3221,21 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -axe-core@^4.6.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0" - integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g== +axe-core@=4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" + integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== axios@^1.1.3, axios@^1.3.4: - version "1.4.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" - integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + version "1.6.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" + integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0" proxy-from-env "^1.1.0" -axobject-query@^3.1.1: +axobject-query@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== @@ -3110,29 +3266,29 @@ babel-plugin-macros@^3.1.0: cosmiconfig "^7.0.0" resolve "^1.19.0" -babel-plugin-polyfill-corejs2@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" - integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== +babel-plugin-polyfill-corejs2@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz#b2df0251d8e99f229a8e60fc4efa9a68b41c8313" + integrity sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.4.3" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52" - integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA== +babel-plugin-polyfill-corejs3@^0.8.5: + version "0.8.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz#25c2d20002da91fe328ff89095c85a391d6856cf" + integrity sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" - core-js-compat "^3.31.0" + "@babel/helper-define-polyfill-provider" "^0.4.3" + core-js-compat "^3.33.1" -babel-plugin-polyfill-regenerator@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" - integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== +babel-plugin-polyfill-regenerator@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz#d4c49e4b44614607c13fb769bcd85c72bb26a4a5" + integrity sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.4.3" bail@^2.0.0: version "2.0.2" @@ -3190,15 +3346,15 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.21.10, browserslist@^4.21.9: - version "4.21.10" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" - integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== +browserslist@^4.21.10, browserslist@^4.21.9, browserslist@^4.22.1: + version "4.22.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" + integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== dependencies: - caniuse-lite "^1.0.30001517" - electron-to-chromium "^1.4.477" + caniuse-lite "^1.0.30001541" + electron-to-chromium "^1.4.535" node-releases "^2.0.13" - update-browserslist-db "^1.0.11" + update-browserslist-db "^1.0.13" buffer-from@^1.0.0: version "1.1.2" @@ -3218,20 +3374,33 @@ builtin-modules@^3.1.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== -busboy@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" - integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== +bundle-require@^3.0.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-3.1.2.tgz#1374a7bdcb8b330a7ccc862ccbf7c137cc43ad27" + integrity sha512-Of6l6JBAxiyQ5axFxUM6dYeP/W7X2Sozeo/4EYB9sJhL+dqL7TKjg+shwxp6jlu/6ZSERfsYtIpSJ1/x3XkAEA== dependencies: - streamsearch "^1.1.0" + load-tsconfig "^0.2.0" -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +bundle-require@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-4.0.2.tgz#65fc74ff14eabbba36d26c9a6161bd78fff6b29e" + integrity sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + load-tsconfig "^0.2.3" + +cac@^6.7.12: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" callsites@^3.0.0: version "3.1.0" @@ -3251,10 +3420,10 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520: - version "1.0.30001520" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001520.tgz#62e2b7a1c7b35269594cf296a80bdf8cb9565006" - integrity sha512-tahF5O9EiiTzwTUqAeFjIZbn4Dnqxzz7ktrgGlMYNLH43Ul26IgTMH/zvL3DG0lZxBYnlT04axvInszUsZULdA== +caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: + 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" @@ -3318,7 +3487,7 @@ character-entities@^2.0.0: resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== -chokidar@^3.5.3: +chokidar@^3.5.1, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -3338,6 +3507,13 @@ chownr@^1.1.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +class-variance-authority@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.0.tgz#1c3134d634d80271b1837452b06d821915954522" + integrity sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A== + dependencies: + clsx "2.0.0" + classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" @@ -3350,16 +3526,21 @@ clean-webpack-plugin@^4.0.0: dependencies: del "^4.1.1" -client-only@0.0.1, client-only@^0.0.1: +client-only@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -clsx@^2.0.0: +clsx@2.0.0, clsx@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + cmdk@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-0.2.0.tgz#53c52d56d8776c8bb8ced1055b5054100c388f7c" @@ -3459,27 +3640,27 @@ constant-case@^3.0.4: tslib "^2.0.3" upper-case "^2.0.2" -convert-source-map@^1.5.0, convert-source-map@^1.7.0: +convert-source-map@^1.5.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== -cookie@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== cookie@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -core-js-compat@^3.31.0: - version "3.32.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.0.tgz#f41574b6893ab15ddb0ac1693681bd56c8550a90" - integrity sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw== +core-js-compat@^3.31.0, core-js-compat@^3.33.1: + version "3.33.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.2.tgz#3ea4563bfd015ad4e4b52442865b02c62aba5085" + integrity sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw== dependencies: - browserslist "^4.21.9" + browserslist "^4.22.1" cosmiconfig@^7.0.0: version "7.1.0" @@ -3497,7 +3678,7 @@ crelt@^1.0.0: resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== -cross-spawn@^7.0.2: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -3511,7 +3692,7 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -css-box-model@^1.2.0: +css-box-model@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== @@ -3646,7 +3827,7 @@ date-fns@^2.0.1, date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -3701,11 +3882,21 @@ deepmerge@^4.2.2, deepmerge@^4.3.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== dependencies: + define-data-property "^1.0.1" has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -3747,6 +3938,13 @@ detect-node-es@^1.1.0: resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== +devlop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -3821,10 +4019,10 @@ ejs@^3.1.6: dependencies: jake "^10.8.5" -electron-to-chromium@^1.4.477: - version "1.4.490" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz#d99286f6e915667fa18ea4554def1aa60eb4d5f1" - integrity sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A== +electron-to-chromium@^1.4.535: + 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" @@ -3876,26 +4074,26 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2: - version "1.22.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" - integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== +es-abstract@^1.22.1: + version "1.22.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.3.tgz#48e79f5573198de6dee3589195727f4f74bc4f32" + integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA== dependencies: array-buffer-byte-length "^1.0.0" - arraybuffer.prototype.slice "^1.0.1" + arraybuffer.prototype.slice "^1.0.2" available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + call-bind "^1.0.5" es-set-tostringtag "^2.0.1" es-to-primitive "^1.2.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.2" get-symbol-description "^1.0.0" globalthis "^1.0.3" gopd "^1.0.1" - has "^1.0.3" has-property-descriptors "^1.0.0" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" internal-slot "^1.0.5" is-array-buffer "^3.0.2" is-callable "^1.2.7" @@ -3903,39 +4101,59 @@ es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2: is-regex "^1.1.4" is-shared-array-buffer "^1.0.2" is-string "^1.0.7" - is-typed-array "^1.1.10" + is-typed-array "^1.1.12" is-weakref "^1.0.2" - object-inspect "^1.12.3" + object-inspect "^1.13.1" object-keys "^1.1.1" object.assign "^4.1.4" - regexp.prototype.flags "^1.5.0" - safe-array-concat "^1.0.0" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" safe-regex-test "^1.0.0" - string.prototype.trim "^1.2.7" - string.prototype.trimend "^1.0.6" - string.prototype.trimstart "^1.0.6" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" typed-array-buffer "^1.0.0" typed-array-byte-length "^1.0.0" typed-array-byte-offset "^1.0.0" typed-array-length "^1.0.4" unbox-primitive "^1.0.2" - which-typed-array "^1.1.10" + which-typed-array "^1.1.13" + +es-iterator-helpers@^1.0.12, es-iterator-helpers@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz#bd81d275ac766431d19305923707c3efd9f1ae40" + integrity sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g== + dependencies: + asynciterator.prototype "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.1" + es-abstract "^1.22.1" + es-set-tostringtag "^2.0.1" + function-bind "^1.1.1" + get-intrinsic "^1.2.1" + globalthis "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + iterator.prototype "^1.1.2" + safe-array-concat "^1.0.1" es-set-tostringtag@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" - integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz#11f7cc9f63376930a5f20be4915834f4bc74f9c9" + integrity sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q== dependencies: - get-intrinsic "^1.1.3" - has "^1.0.3" + get-intrinsic "^1.2.2" has-tostringtag "^1.0.0" + hasown "^2.0.0" es-shim-unscopables@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== dependencies: - has "^1.0.3" + hasown "^2.0.0" es-to-primitive@^1.2.1: version "1.2.1" @@ -3946,6 +4164,161 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +esbuild-android-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" + integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== + +esbuild-android-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" + integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== + +esbuild-darwin-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" + integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== + +esbuild-darwin-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" + integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== + +esbuild-freebsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" + integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== + +esbuild-freebsd-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" + integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== + +esbuild-linux-32@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" + integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== + +esbuild-linux-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" + integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== + +esbuild-linux-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" + integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== + +esbuild-linux-arm@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" + integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== + +esbuild-linux-mips64le@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" + integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== + +esbuild-linux-ppc64le@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" + integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== + +esbuild-linux-riscv64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" + integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== + +esbuild-linux-s390x@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" + integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== + +esbuild-netbsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" + integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== + +esbuild-openbsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" + integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== + +esbuild-sunos-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" + integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== + +esbuild-windows-32@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" + integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== + +esbuild-windows-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" + integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== + +esbuild-windows-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" + integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== + +esbuild@^0.14.25: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" + integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== + optionalDependencies: + "@esbuild/linux-loong64" "0.14.54" + esbuild-android-64 "0.14.54" + esbuild-android-arm64 "0.14.54" + esbuild-darwin-64 "0.14.54" + esbuild-darwin-arm64 "0.14.54" + esbuild-freebsd-64 "0.14.54" + esbuild-freebsd-arm64 "0.14.54" + esbuild-linux-32 "0.14.54" + esbuild-linux-64 "0.14.54" + esbuild-linux-arm "0.14.54" + esbuild-linux-arm64 "0.14.54" + esbuild-linux-mips64le "0.14.54" + esbuild-linux-ppc64le "0.14.54" + esbuild-linux-riscv64 "0.14.54" + esbuild-linux-s390x "0.14.54" + esbuild-netbsd-64 "0.14.54" + esbuild-openbsd-64 "0.14.54" + esbuild-sunos-64 "0.14.54" + esbuild-windows-32 "0.14.54" + esbuild-windows-64 "0.14.54" + esbuild-windows-arm64 "0.14.54" + +esbuild@^0.18.2: + version "0.18.20" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" + integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== + optionalDependencies: + "@esbuild/android-arm" "0.18.20" + "@esbuild/android-arm64" "0.18.20" + "@esbuild/android-x64" "0.18.20" + "@esbuild/darwin-arm64" "0.18.20" + "@esbuild/darwin-x64" "0.18.20" + "@esbuild/freebsd-arm64" "0.18.20" + "@esbuild/freebsd-x64" "0.18.20" + "@esbuild/linux-arm" "0.18.20" + "@esbuild/linux-arm64" "0.18.20" + "@esbuild/linux-ia32" "0.18.20" + "@esbuild/linux-loong64" "0.18.20" + "@esbuild/linux-mips64el" "0.18.20" + "@esbuild/linux-ppc64" "0.18.20" + "@esbuild/linux-riscv64" "0.18.20" + "@esbuild/linux-s390x" "0.18.20" + "@esbuild/linux-x64" "0.18.20" + "@esbuild/netbsd-x64" "0.18.20" + "@esbuild/openbsd-x64" "0.18.20" + "@esbuild/sunos-x64" "0.18.20" + "@esbuild/win32-arm64" "0.18.20" + "@esbuild/win32-ia32" "0.18.20" + "@esbuild/win32-x64" "0.18.20" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -4006,19 +4379,34 @@ eslint-config-next@13.2.1: eslint-plugin-react "^7.31.7" eslint-plugin-react-hooks "^4.5.0" +eslint-config-next@13.2.4: + version "13.2.4" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.2.4.tgz#8aa4d42da3a575a814634ba9c88c8d25266c5fdd" + integrity sha512-lunIBhsoeqw6/Lfkd6zPt25w1bn0znLA/JCL+au1HoEpSb4/PpsOYsYtgV/q+YPsoKIOzFyU5xnb04iZnXjUvg== + dependencies: + "@next/eslint-plugin-next" "13.2.4" + "@rushstack/eslint-patch" "^1.1.3" + "@typescript-eslint/parser" "^5.42.0" + eslint-import-resolver-node "^0.3.6" + eslint-import-resolver-typescript "^3.5.2" + eslint-plugin-import "^2.26.0" + eslint-plugin-jsx-a11y "^6.5.1" + eslint-plugin-react "^7.31.7" + eslint-plugin-react-hooks "^4.5.0" + eslint-config-prettier@^8.3.0: version "8.10.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== eslint-config-turbo@latest: - version "1.10.12" - resolved "https://registry.yarnpkg.com/eslint-config-turbo/-/eslint-config-turbo-1.10.12.tgz#5868252d6833dd2b5cab4414751ed31ebe2177c3" - integrity sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA== + version "1.10.16" + resolved "https://registry.yarnpkg.com/eslint-config-turbo/-/eslint-config-turbo-1.10.16.tgz#175cabd8eb1ec5fb8e254abcb09df3955fe476a1" + integrity sha512-O3NQI72bQHV7FvSC6lWj66EGx8drJJjuT1kuInn6nbMLOHdMBhSUX/8uhTAlHRQdlxZk2j9HtgFCIzSc93w42g== dependencies: - eslint-plugin-turbo "1.10.12" + eslint-plugin-turbo "1.10.16" -eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.7: +eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== @@ -4039,9 +4427,9 @@ eslint-import-resolver-typescript@^2.7.1: tsconfig-paths "^3.14.1" eslint-import-resolver-typescript@^3.5.2: - version "3.6.0" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.0.tgz#36f93e1eb65a635e688e16cae4bead54552e3bbd" - integrity sha512-QTHR9ddNnn35RTxlaEnx2gCxqFlF2SEN0SE2d17SqwyM7YOSI2GHWRYp5BiRkObTUNYPupC/3Fq2a0PpT+EKpg== + version "3.6.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" + integrity sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg== dependencies: debug "^4.3.4" enhanced-resolve "^5.12.0" @@ -4059,50 +4447,49 @@ eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: debug "^3.2.7" eslint-plugin-import@^2.26.0: - version "2.28.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz#8d66d6925117b06c4018d491ae84469eb3cb1005" - integrity sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q== - dependencies: - array-includes "^3.1.6" - array.prototype.findlastindex "^1.2.2" - array.prototype.flat "^1.3.1" - array.prototype.flatmap "^1.3.1" + version "2.29.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz#8133232e4329ee344f2f612885ac3073b0b7e155" + integrity sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.7" + eslint-import-resolver-node "^0.3.9" eslint-module-utils "^2.8.0" - has "^1.0.3" - is-core-module "^2.12.1" + hasown "^2.0.0" + is-core-module "^2.13.1" is-glob "^4.0.3" minimatch "^3.1.2" - object.fromentries "^2.0.6" - object.groupby "^1.0.0" - object.values "^1.1.6" - resolve "^1.22.3" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" semver "^6.3.1" tsconfig-paths "^3.14.2" eslint-plugin-jsx-a11y@^6.5.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976" - integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== - dependencies: - "@babel/runtime" "^7.20.7" - aria-query "^5.1.3" - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - ast-types-flow "^0.0.7" - axe-core "^4.6.2" - axobject-query "^3.1.1" + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" + integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== + dependencies: + "@babel/runtime" "^7.23.2" + aria-query "^5.3.0" + array-includes "^3.1.7" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "=4.7.0" + axobject-query "^3.2.1" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - has "^1.0.3" - jsx-ast-utils "^3.3.3" - language-tags "=1.0.5" + es-iterator-helpers "^1.0.15" + hasown "^2.0.0" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - semver "^6.3.0" + object.entries "^1.1.7" + object.fromentries "^2.0.7" eslint-plugin-react-hooks@^4.5.0: version "4.6.0" @@ -4130,14 +4517,15 @@ eslint-plugin-react@7.31.8: string.prototype.matchall "^4.0.7" eslint-plugin-react@^7.29.4, eslint-plugin-react@^7.31.7: - version "7.33.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.1.tgz#bc27cccf860ae45413a4a4150bf0977345c1ceab" - integrity sha512-L093k0WAMvr6VhNwReB8VgOq5s2LesZmrpPdKz/kZElQDzqS7G7+DnKoqT+w4JwuiGeAhAvHO0fvy0Eyk4ejDA== + version "7.33.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" + integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== dependencies: array-includes "^3.1.6" array.prototype.flatmap "^1.3.1" array.prototype.tosorted "^1.1.1" doctrine "^2.1.0" + es-iterator-helpers "^1.0.12" estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" @@ -4150,10 +4538,10 @@ eslint-plugin-react@^7.29.4, eslint-plugin-react@^7.31.7: semver "^6.3.1" string.prototype.matchall "^4.0.8" -eslint-plugin-turbo@1.10.12: - version "1.10.12" - resolved "https://registry.yarnpkg.com/eslint-plugin-turbo/-/eslint-plugin-turbo-1.10.12.tgz#3f95884faf35b56e0855d939585fa6cd457bb128" - integrity sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw== +eslint-plugin-turbo@1.10.16: + version "1.10.16" + resolved "https://registry.yarnpkg.com/eslint-plugin-turbo/-/eslint-plugin-turbo-1.10.16.tgz#368723f2140617f273c60f65b09267f73b496cd9" + integrity sha512-ZjrR88MTN64PNGufSEcM0tf+V1xFYVbeiMeuIqr0aiABGomxFLo4DBkQ7WI4WzkZtWQSIA2sP+yxqSboEfL9MQ== dependencies: dotenv "16.0.3" @@ -4247,6 +4635,52 @@ eslint@8.34.0: strip-json-comments "^3.1.0" text-table "^0.2.0" +eslint@8.36.0: + version "8.36.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf" + integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.1" + "@eslint/js" "8.36.0" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-visitor-keys "^3.3.0" + espree "^9.5.0" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + eslint@^7.23.0, eslint@^7.32.0: version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" @@ -4294,17 +4728,18 @@ eslint@^7.23.0, eslint@^7.32.0: v8-compile-cache "^2.0.3" eslint@^8.31.0: - version "8.47.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.47.0.tgz#c95f9b935463fb4fad7005e626c7621052e90806" - integrity sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q== + 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.47.0" - "@humanwhocodes/config-array" "^0.11.10" + "@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" + "@ungap/structured-clone" "^1.2.0" ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -4345,7 +4780,7 @@ espree@^7.3.0, espree@^7.3.1: acorn-jsx "^5.3.1" eslint-visitor-keys "^1.3.0" -espree@^9.4.0, espree@^9.6.0, espree@^9.6.1: +espree@^9.4.0, espree@^9.5.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -4398,6 +4833,26 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +eventsource-parser@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-0.1.0.tgz#4a6b84751ca8e704040e6f7f50e7d77344fa1b7c" + integrity sha512-M9QjFtEIkwytUarnx113HGmgtk52LSn3jNAtnWKi3V+b9rqSfQeVdLsaD5AG/O4IrGQwmAAHBIsqbmURPTd2rA== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" @@ -4414,11 +4869,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-fifo@^1.1.0, fast-fifo@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.0.tgz#03e381bcbfb29932d7c3afde6e15e83e05ab4d8b" - integrity sha512-IgfweLvEpwyA4WgiQe9Nx6VV2QkML2NkvZnk1oKnIzXgXdWxuhF7zw4DvLTPZJn6PIUneiAXPF24QmoEqHTjyw== + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== -fast-glob@^3.2.12, fast-glob@^3.2.9, fast-glob@^3.3.1: +fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== @@ -4458,7 +4913,15 @@ file-entry-cache@^6.0.1: resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: - flat-cache "^3.0.4" + flat-cache "^3.0.4" + +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" file-selector@^0.6.0: version "0.6.0" @@ -4512,22 +4975,23 @@ find-up@^5.0.0: path-exists "^4.0.0" flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.1.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.1.tgz#a02a15fdec25a8f844ff7cc658f03dd99eb4609b" + integrity sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q== dependencies: - flatted "^3.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flatted@^3.2.9: + version "3.2.9" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== follow-redirects@^1.15.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== for-each@^0.3.3: version "0.3.3" @@ -4550,10 +5014,10 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== +fraction.js@^4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== framework-utils@^1.1.0: version "1.1.0" @@ -4581,31 +5045,31 @@ fs.realpath@^1.0.0: integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" - integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - functions-have-names "^1.2.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== -functions-have-names@^1.2.2, functions-have-names@^1.2.3: +functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -4616,22 +5080,22 @@ gensync@^1.0.0-beta.2: integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== gesto@^1.19.0, gesto@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/gesto/-/gesto-1.19.1.tgz#b2a29730663eecf77b248982bbff929e79d4a461" - integrity sha512-ofWVEdqmnpFm3AFf7aoclhoayseb3OkwSiXbXusKYu/99iN5HgeWP+SWqdghQ5TFlOgP5Zlz+6SY8mP2V0kFaQ== + version "1.19.2" + resolved "https://registry.yarnpkg.com/gesto/-/gesto-1.19.2.tgz#1f5ab5cf49bd5a1ec2ee3805f35e77161343ea7b" + integrity sha512-i6OGsrR2GN7n2dQaUhY+LZ+AAZgBNg5/1kest/ST5VRRflfVl5bSfvOkJMDmKEUrGbKF6eIDkFz/DnCXJD4UMA== dependencies: "@daybrush/utils" "^1.13.0" "@scena/event-emitter" "^1.0.2" -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-nonce@^1.0.0: version "1.0.1" @@ -4643,6 +5107,11 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -4652,9 +5121,9 @@ get-symbol-description@^1.0.0: get-intrinsic "^1.1.1" get-tsconfig@^4.5.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.0.tgz#06ce112a1463e93196aa90320c35df5039147e34" - integrity sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw== + version "4.7.2" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.2.tgz#0dcd6fb330391d46332f4c6c1bf89a6514c2ddce" + integrity sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A== dependencies: resolve-pkg-maps "^1.0.0" @@ -4677,11 +5146,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - glob@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -4735,9 +5199,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0, globals@^13.6.0, globals@^13.9.0: - version "13.21.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.21.0.tgz#163aae12f34ef502f5153cfbdd3600f36c63c571" - integrity sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg== + version "13.23.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.23.0.tgz#ef31673c926a0976e1f61dab4dca57e0c0a8af02" + integrity sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA== dependencies: type-fest "^0.20.2" @@ -4748,7 +5212,7 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globby@^11.0.4, globby@^11.1.0: +globby@^11.0.3, globby@^11.0.4, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -4778,7 +5242,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4814,11 +5278,11 @@ has-flag@^4.0.0: integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== dependencies: - get-intrinsic "^1.1.1" + get-intrinsic "^1.2.2" has-proto@^1.0.1: version "1.0.1" @@ -4837,12 +5301,12 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" hast-util-whitespace@^2.0.0: version "2.0.1" @@ -4857,7 +5321,12 @@ header-case@^2.0.4: capital-case "^1.0.4" tslib "^2.0.3" -highlight.js@^11.8.0, highlight.js@~11.8.0: +highlight.js@^11.8.0, highlight.js@~11.9.0: + version "11.9.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0" + integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw== + +highlight.js@~11.8.0: version "11.8.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.8.0.tgz#966518ea83257bae2e7c9a48596231856555bb65" integrity sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg== @@ -4877,6 +5346,11 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + idb@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" @@ -4938,13 +5412,13 @@ inline-style-parser@0.1.1: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== -internal-slot@^1.0.3, internal-slot@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" - integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== +internal-slot@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" + integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg== dependencies: - get-intrinsic "^1.2.0" - has "^1.0.3" + get-intrinsic "^1.2.2" + hasown "^2.0.0" side-channel "^1.0.4" internmap@^1.0.0: @@ -4986,6 +5460,13 @@ is-arrayish@^0.3.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -5018,14 +5499,14 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.11.0, is-core-module@^2.12.1, is-core-module@^2.13.0, is-core-module@^2.9.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== +is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" + hasown "^2.0.0" -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -5044,11 +5525,25 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -5056,6 +5551,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -5139,6 +5639,11 @@ is-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== +is-set@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + is-shared-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" @@ -5165,13 +5670,18 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typed-array@^1.1.10, is-typed-array@^1.1.9: +is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: version "1.1.12" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== dependencies: which-typed-array "^1.1.11" +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -5179,6 +5689,14 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -5194,6 +5712,17 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + jake@^10.8.5: version "10.8.7" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" @@ -5222,10 +5751,15 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" -jiti@^1.18.2: - version "1.19.1" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.1.tgz#fa99e4b76a23053e0e7cde098efe1704a14c16f1" - integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== +jiti@^1.19.1: + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + +joycon@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== js-cookie@^3.0.1: version "3.0.5" @@ -5267,6 +5801,11 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -5299,7 +5838,7 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.1.2, json5@^2.2.0, json5@^2.2.2: +json5@^2.1.2, json5@^2.2.0, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -5318,7 +5857,7 @@ jsonpointer@^5.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== @@ -5328,6 +5867,13 @@ jsonpointer@^5.0.0: object.assign "^4.1.4" object.values "^1.1.6" +jsx-dom-cjs@^8.0.3: + version "8.0.7" + resolved "https://registry.yarnpkg.com/jsx-dom-cjs/-/jsx-dom-cjs-8.0.7.tgz#098c54680ebf5bb6f6d12cdea5cde3799c172212" + integrity sha512-dQWnuQ+bTm7o72ZlJU4glzeMX8KLxx5U+ZwmEAzVP1+roL7BSM0MrkWdHjdsuNgmxobZCJ+qgiot9EgbJPOoEg== + dependencies: + csstype "^3.1.2" + keycode@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" @@ -5343,22 +5889,29 @@ keycon@^1.2.0: "@scena/event-emitter" "^1.0.2" keycode "^2.2.0" +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kleur@^4.0.3: version "4.1.5" resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -language-subtag-registry@~0.3.2: +language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== -language-tags@=1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" - integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== dependencies: - language-subtag-registry "~0.3.2" + language-subtag-registry "^0.3.20" leven@^3.1.0: version "3.1.0" @@ -5402,6 +5955,11 @@ linkifyjs@^4.1.0: resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.1.tgz#73d427e3bbaaf4ca8e71c589ad4ffda11a9a5fde" integrity sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA== +load-tsconfig@^0.2.0, load-tsconfig@^0.2.3: + version "0.2.5" + resolved "https://registry.yarnpkg.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz#453b8cd8961bfb912dea77eb6c168fe8cca3d3a1" + integrity sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg== + loader-utils@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" @@ -5495,6 +6053,15 @@ lowlight@^2.9.0: fault "^2.0.0" highlight.js "~11.8.0" +lowlight@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-3.1.0.tgz#aa394c5f3a7689fce35fa49a7c850ba3ead4f590" + integrity sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.0.0" + highlight.js "~11.9.0" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -5509,16 +6076,21 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== +lucide-react@^0.244.0: + version "0.244.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.244.0.tgz#9626f44881830280012dad23afda7ddbcffff24b" + integrity sha512-PeDVbx5PlIRrVvdxiuSxPfBo7sK5qrL3LbvvRoGVNiHYRAkBm/48lKqoioxcmp0bgsyJs9lMw7CdtGFvnMJbVg== lucide-react@^0.263.1: version "0.263.1" resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.263.1.tgz#a456ee0d171aa373929bd3ee20d6f9fb4429c301" integrity sha512-keqxAx97PlaEN89PXZ6ki1N8nRjGWtDa4021GFYLNj0RgruM5odbpl8GHTExj0hhPq3sF6Up0gnxt6TSHu+ovw== +lucide-react@^0.274.0: + version "0.274.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.274.0.tgz#d3b54dcb972b12f1292061448d61d422ef2e269d" + integrity sha512-qiWcojRXEwDiSimMX1+arnxha+ROJzZjJaVvCC0rsG6a9pUPjZePXSq7em4ZKMp0NDm1hyzPNkM7UaWC3LU2AA== + magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" @@ -5551,9 +6123,9 @@ markdown-it-task-lists@^2.1.1: integrity sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA== markdown-it@^13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" - integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== + version "13.0.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.2.tgz#1bc22e23379a6952e5d56217fbed881e0c94d536" + integrity sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w== dependencies: argparse "^2.0.1" entities "~3.0.1" @@ -5619,10 +6191,10 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== -memoize-one@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== merge-stream@^2.0.0: version "2.0.0" @@ -5841,13 +6413,18 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12: +mime-types@^2.1.12, mime-types@^2.1.27: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -5885,16 +6462,16 @@ mkdirp@^0.5.5: minimist "^1.2.6" mobx-react-lite@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.3.tgz#f7aa5ac3be558ca19a53b2929d9599679769c2a8" - integrity sha512-wEE1oT5zvDdvplG4HnRrFgPwg5GFVVrEtl42Er85k23zeu3om8H8wbDPgdbQP88zAihVsik6xJfw6VnzUl8fQw== + version "4.0.5" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.5.tgz#e2cb98f813e118917bcc463638f5bf6ea053a67b" + integrity sha512-StfB2wxE8imKj1f6T8WWPf4lVMx3cYH9Iy60bbKXEs21+HQ4tvvfIBZfSmMXgQAefi8xYEwQIz4GN9s0d2h7dg== dependencies: use-sync-external-store "^1.2.0" mobx@^6.10.0: - version "6.10.0" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.10.0.tgz#3537680fe98d45232cc19cc8f76280bd8bb6b0b7" - integrity sha512-WMbVpCMFtolbB8swQ5E2YRrU+Yu8iLozCVx3CdGjbBKlP7dFiCSuiG06uea3JCFN5DnvtAX7+G5Bp82e2xu0ww== + version "6.10.2" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.10.2.tgz#96e123deef140750360ca9a5b02a8b91fbffd4d9" + integrity sha512-B1UGC3ieK3boCjnMEcZSwxqRDMdzX65H/8zOHbuTY8ZhvrIjTUoLRR2TP2bPqIgYRfb3+dUigu8yMZufNjn0LQ== mri@^1.1.0: version "1.2.0" @@ -5921,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" @@ -5940,6 +6517,14 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +next-images@^1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/next-images/-/next-images-1.8.5.tgz#2eb5535bb1d6c58a5c4e03bc3be6c72c8a053a45" + integrity sha512-YLBERp92v+Nu2EVxI9+wa32KRuxyxTC8ItbiHUWVPlatUoTl0yRqsNtP39c2vYv27VRvY4LlYcUGjNRBSMUIZA== + dependencies: + file-loader "^6.2.0" + url-loader "^4.1.0" + next-pwa@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4" @@ -5983,30 +6568,6 @@ next@12.3.2: "@next/swc-win32-ia32-msvc" "12.3.2" "@next/swc-win32-x64-msvc" "12.3.2" -next@^13.4.16: - version "13.4.16" - resolved "https://registry.yarnpkg.com/next/-/next-13.4.16.tgz#327ef6885b22161ed001cd5943c20b5e409a9406" - integrity sha512-1xaA/5DrfpPu0eV31Iro7JfPeqO8uxQWb1zYNTe+KDKdzqkAGapLcDYHMLNKXKB7lHjZ7LfKUOf9dyuzcibrhA== - dependencies: - "@next/env" "13.4.16" - "@swc/helpers" "0.5.1" - busboy "1.6.0" - caniuse-lite "^1.0.30001406" - postcss "8.4.14" - styled-jsx "5.1.1" - watchpack "2.4.0" - zod "3.21.4" - optionalDependencies: - "@next/swc-darwin-arm64" "13.4.16" - "@next/swc-darwin-x64" "13.4.16" - "@next/swc-linux-arm64-gnu" "13.4.16" - "@next/swc-linux-arm64-musl" "13.4.16" - "@next/swc-linux-x64-gnu" "13.4.16" - "@next/swc-linux-x64-musl" "13.4.16" - "@next/swc-win32-arm64-msvc" "13.4.16" - "@next/swc-win32-ia32-msvc" "13.4.16" - "@next/swc-win32-x64-msvc" "13.4.16" - no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -6016,9 +6577,9 @@ no-case@^3.0.4: tslib "^2.0.3" node-abi@^3.3.0: - version "3.45.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.45.0.tgz#f568f163a3bfca5aacfce1fbeee1fa2cc98441f5" - integrity sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ== + version "3.51.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.51.0.tgz#970bf595ef5a26a271307f8a4befa02823d4e87d" + integrity sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA== dependencies: semver "^7.3.5" @@ -6028,9 +6589,9 @@ node-addon-api@^6.1.0: integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== node-fetch@^2.6.7: - version "2.6.12" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" - integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" @@ -6054,6 +6615,13 @@ normalize.css@^8.0.1: resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + nprogress@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" @@ -6069,10 +6637,10 @@ object-hash@^3.0.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-inspect@^1.12.3, object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1, object-inspect@^1.9.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== object-is@^1.0.1: version "1.1.5" @@ -6097,41 +6665,41 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.5, object.entries@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23" - integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== +object.entries@^1.1.5, object.entries@^1.1.6, object.entries@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" + integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" -object.fromentries@^2.0.5, object.fromentries@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" - integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== +object.fromentries@^2.0.5, object.fromentries@^2.0.6, object.fromentries@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" + integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" -object.groupby@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.0.tgz#cb29259cf90f37e7bac6437686c1ea8c916d12a9" - integrity sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw== +object.groupby@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee" + integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ== dependencies: call-bind "^1.0.2" define-properties "^1.2.0" - es-abstract "^1.21.2" + es-abstract "^1.22.1" get-intrinsic "^1.2.1" object.hasown@^1.1.1, object.hasown@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92" - integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" + integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== dependencies: - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" object.omit@^3.0.0: version "3.0.0" @@ -6147,14 +6715,14 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.5, object.values@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" - integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== +object.values@^1.1.5, object.values@^1.1.6, object.values@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" + integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -6163,6 +6731,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + optionator@^0.9.1, optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -6281,7 +6856,7 @@ path-is-inside@^1.0.2: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -6361,6 +6936,14 @@ postcss-js@^4.0.1: dependencies: camelcase-css "^2.0.1" +postcss-load-config@^3.0.1: + version "3.1.4" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" + integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== + dependencies: + lilconfig "^2.0.5" + yaml "^1.10.2" + postcss-load-config@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd" @@ -6406,10 +6989,10 @@ postcss@8.4.14: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.14, postcss@^8.4.21, postcss@^8.4.23: - version "8.4.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" - integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== +postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.29: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" @@ -6438,15 +7021,25 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.8.7: +prettier-plugin-tailwindcss@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.3.0.tgz#8299b307c7f6467f52732265579ed9375be6c818" + integrity sha512-009/Xqdy7UmkcTBpwlq7jsViDqXAYSOMLDrHAdTMlVZOrKfM2o9Ci7EMWTMZ7SkKBFTG04UM9F9iM2+4i6boDA== + +prettier-plugin-tailwindcss@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.6.tgz#8e511857a49bf127f078985f52b04a70e8e92285" + integrity sha512-2Xgb+GQlkPAUCFi3sV+NOYcSI5XgduvDBL2Zt/hwJudeKXkyvRS65c38SB0yb9UB40+1rL83I6m0RtlOQ8eHdg== + +prettier@^2.8.7, prettier@^2.8.8: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== prettier@latest: - version "3.0.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.1.tgz#65271fc9320ce4913c57747a70ce635b30beaa40" - integrity sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ== + version "3.0.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" @@ -6468,9 +7061,9 @@ prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, react-is "^16.13.1" property-information@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d" - integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg== + version "6.4.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.4.0.tgz#6bc4c618b0c2d68b3bb8b552cbb97f8e300a0f82" + integrity sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ== prosemirror-changeset@^2.2.0: version "2.2.1" @@ -6549,23 +7142,16 @@ prosemirror-markdown@^1.10.1, prosemirror-markdown@^1.11.1: prosemirror-model "^1.0.0" prosemirror-menu@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.2.tgz#c545a2de0b8cb79babc07682b1d93de0f273aa33" - integrity sha512-437HIWTq4F9cTX+kPfqZWWm+luJm95Aut/mLUy+9OMrOml0bmWDS26ceC6SNfb2/S94et1sZ186vLO7pDHzxSw== + version "1.2.4" + resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz#3cfdc7c06d10f9fbd1bce29082c498bd11a0a79a" + integrity sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA== dependencies: crelt "^1.0.0" prosemirror-commands "^1.0.0" prosemirror-history "^1.0.0" prosemirror-state "^1.0.0" -prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" - integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== - dependencies: - orderedmap "^2.0.0" - -prosemirror-model@^1.19.0: +prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1: version "1.19.3" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006" integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ== @@ -6618,16 +7204,16 @@ prosemirror-trailing-node@^2.0.2: escape-string-regexp "^4.0.0" prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.0, prosemirror-transform@^1.7.3: - version "1.7.4" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.7.4.tgz#ea878c90563f3586064dd5ccf6cabb50b2753fd9" - integrity sha512-GO38mvqJ2yeI0BbL5E1CdHcly032Dlfn9nHqlnCHqlNf9e9jZwJixxp6VRtOeDZ1uTDpDIziezMKbA41LpAx3A== + version "1.8.0" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz#a47c64a3c373c1bd0ff46e95be3210c8dda0cd11" + integrity sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A== dependencies: prosemirror-model "^1.0.0" prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.28.2, prosemirror-view@^1.31.0: - version "1.31.7" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.31.7.tgz#dccb2879314e1e1a24d48044c15374754e50ef00" - integrity sha512-Pr7w93yOYmxQwzGIRSaNLZ/1uM6YjnenASzN2H6fO6kGekuzRbgZ/4bHbBTd1u4sIQmL33/TcGmzxxidyPwCjg== + version "1.32.3" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.32.3.tgz#9019151211f685e553f681837435cae4885beece" + integrity sha512-tP7AUTxisM0m3PDxs6vDWgTjgcbFo4fnwg2M/5NHlgMqUJgBNOqSUZETBZKmLD7AxGN3GZ5yqEzQv9r9VCZKrg== dependencies: prosemirror-model "^1.16.0" prosemirror-state "^1.0.0" @@ -6647,9 +7233,9 @@ pump@^3.0.0: once "^1.3.1" punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== queue-microtask@^1.2.2: version "1.2.3" @@ -6661,7 +7247,7 @@ queue-tick@^1.0.1: resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== -raf-schd@^4.0.2: +raf-schd@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== @@ -6683,19 +7269,6 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-beautiful-dnd@^13.1.1: - version "13.1.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" - integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== - dependencies: - "@babel/runtime" "^7.9.2" - css-box-model "^1.2.0" - memoize-one "^5.1.1" - raf-schd "^4.0.2" - react-redux "^7.2.0" - redux "^4.0.4" - use-memo-one "^1.1.1" - react-color@^2.19.3: version "2.19.3" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" @@ -6718,15 +7291,15 @@ react-css-styled@^1.1.9: framework-utils "^1.1.0" react-datepicker@^4.8.0: - version "4.16.0" - resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.16.0.tgz#b9dd389bb5611a1acc514bba1dd944be21dd877f" - integrity sha512-hNQ0PAg/LQoVbDUO/RWAdm/RYmPhN3cz7LuQ3hqbs24OSp69QCiKOJRrQ4jk1gv1jNR5oYu8SjjgfDh8q6Q1yw== + version "4.21.0" + resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.21.0.tgz#09b2ba3e53654a8563b22c9f649835716c8456d3" + integrity sha512-z0DtuRrKMz9i7dcTusW29VacbM9pn08g1yw0cG+Y5GpodJDxSWv7zUMxl3IwKN9Ap/AMphiepvmT5P+iNCgEiA== dependencies: "@popperjs/core" "^2.11.8" classnames "^2.2.6" date-fns "^2.30.0" prop-types "^15.7.2" - react-onclickoutside "^6.12.2" + react-onclickoutside "^6.13.0" react-popper "^2.3.0" react-dom@18.2.0, react-dom@^18.2.0: @@ -6752,20 +7325,15 @@ react-fast-compare@^3.0.1: integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== react-hook-form@^7.38.0: - version "7.45.4" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.45.4.tgz#73d228b704026ae95d7e5f7b207a681b173ec62a" - integrity sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ== + 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" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - react-is@^18.0.0, react-is@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" @@ -6797,10 +7365,10 @@ react-markdown@^8.0.7: unist-util-visit "^4.0.0" vfile "^5.0.0" -react-moveable@^0.54.1: - version "0.54.1" - resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.1.tgz#3c69748c444184700e6999501b0da953c934205e" - integrity sha512-Kj2ifw9nk3LZvu7ezhst8Z5WBPRr+yVv9oROwrBirFlHmwGHHZXUGk5Gaezu+JGqqNRsQJncVMW5Uf68KSSOvg== +react-moveable@^0.54.2: + version "0.54.2" + resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.2.tgz#87ce9af3499dc1c8218bce7e174b10264c1bbecf" + integrity sha512-NGaVLbn0i9pb3+BWSKGWFqI/Mgm4+WMeWHxXXQ4Qi1tHxWCXrUrbGvpxEpt69G/hR7dez+/m68ex+fabjnvcUg== dependencies: "@daybrush/utils" "^1.13.0" "@egjs/agent" "^2.2.1" @@ -6816,7 +7384,7 @@ react-moveable@^0.54.1: react-css-styled "^1.1.9" react-selecto "^1.25.0" -react-onclickoutside@^6.12.2: +react-onclickoutside@^6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc" integrity sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A== @@ -6842,17 +7410,17 @@ react-popper@^2.2.5, react-popper@^2.3.0: react-fast-compare "^3.0.1" warning "^4.0.2" -react-redux@^7.2.0: - version "7.2.9" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" - integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== +react-redux@^8.1.1: + version "8.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" + integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== dependencies: - "@babel/runtime" "^7.15.4" - "@types/react-redux" "^7.1.20" + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^17.0.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" react-remove-scroll-bar@^2.3.3: version "2.3.4" @@ -6936,17 +7504,29 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -redux@^4.0.0, redux@^4.0.4: +redux@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== dependencies: "@babel/runtime" "^7.9.2" +reflect.getprototypeof@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz#aaccbf41aca3821b87bb71d9dcbc7ad0ba50a3f3" + integrity sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== dependencies: regenerate "^1.4.2" @@ -6967,14 +7547,14 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" - integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== dependencies: call-bind "^1.0.2" define-properties "^1.2.0" - functions-have-names "^1.2.3" + set-function-name "^2.0.0" regexpp@^3.1.0, regexpp@^3.2.0: version "3.2.0" @@ -7029,26 +7609,31 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-pkg-maps@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.0, resolve@^1.22.2, resolve@^1.22.3, resolve@^1.22.4: - version "1.22.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" - integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== +resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.0, resolve@^1.22.2, resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" resolve@^2.0.0-next.3, resolve@^2.0.0-next.4: - version "2.0.0-next.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" - integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -7088,13 +7673,20 @@ rollup@2.78.0: optionalDependencies: fsevents "~2.3.2" -rollup@^2.43.1: +rollup@^2.43.1, rollup@^2.74.1: version "2.79.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== optionalDependencies: fsevents "~2.3.2" +rollup@^3.2.5: + version "3.29.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" + integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + optionalDependencies: + fsevents "~2.3.2" + rope-sequence@^1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425" @@ -7114,13 +7706,13 @@ sade@^1.7.3: dependencies: mri "^1.1.0" -safe-array-concat@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" - integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== +safe-array-concat@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== dependencies: call-bind "^1.0.2" - get-intrinsic "^1.2.0" + get-intrinsic "^1.2.1" has-symbols "^1.0.3" isarray "^2.0.5" @@ -7154,7 +7746,7 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.1.1: +schema-utils@^3.0.0, schema-utils@^3.1.1: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -7214,10 +7806,29 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +set-function-name@^2.0.0, set-function-name@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + sharp@^0.32.1: - version "0.32.4" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.4.tgz#0354653b7924f2520b2264ac9bcd10a58bf411b6" - integrity sha512-exUnZewqVZC6UXqXuQ8fyJJv0M968feBi04jb9GcUHrWtkRoAKnbJt8IfwT4NJs7FskArbJ14JAFGVuooszoGg== + version "0.32.6" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.6.tgz#6ad30c0b7cd910df65d5f355f774aa4fce45732a" + integrity sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w== dependencies: color "^4.2.3" detect-libc "^2.0.2" @@ -7249,6 +7860,11 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" @@ -7292,11 +7908,6 @@ snake-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" -sonner@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/sonner/-/sonner-0.6.2.tgz#d87420e80d8b25b6d2bd6aabcc28465f03962bdc" - integrity sha512-bh4FWhYoNN481ZIW94W4e0kSLBTMGislYg2YXvDS1px1AJJz4erQe9jHV8s5pS1VMVDgfh3CslNSFLaU6Ldrnw== - source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -7315,6 +7926,13 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" +source-map@0.8.0-beta.0, source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -7325,13 +7943,6 @@ source-map@^0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.8.0-beta.0: - version "0.8.0-beta.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" - integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== - dependencies: - whatwg-url "^7.0.0" - sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" @@ -7354,15 +7965,10 @@ stacktrace-parser@^0.1.10: dependencies: type-fest "^0.7.1" -streamsearch@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" - integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== - streamx@^2.15.0: - version "2.15.1" - resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.1.tgz#396ad286d8bc3eeef8f5cea3f029e81237c024c6" - integrity sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA== + version "2.15.2" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.2.tgz#680eacebdc9c43ede7362c2e6695b34dd413c741" + integrity sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg== dependencies: fast-fifo "^1.1.0" queue-tick "^1.0.1" @@ -7377,45 +7983,46 @@ string-width@^4.2.3: strip-ansi "^6.0.1" string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.7, string.prototype.matchall@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" - integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== + version "4.0.10" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" + integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - get-intrinsic "^1.1.3" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" has-symbols "^1.0.3" - internal-slot "^1.0.3" - regexp.prototype.flags "^1.4.3" + internal-slot "^1.0.5" + regexp.prototype.flags "^1.5.0" + set-function-name "^2.0.0" side-channel "^1.0.4" -string.prototype.trim@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" - integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" -string.prototype.trimend@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" - integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== +string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" -string.prototype.trimstart@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" - integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== +string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" string_decoder@^1.1.1: version "1.3.0" @@ -7450,6 +8057,11 @@ strip-comments@^2.0.1: resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -7461,9 +8073,9 @@ strip-json-comments@~2.0.1: integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== style-to-object@^0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.2.tgz#a8247057111dea8bd3b8a1a66d2d0c9cf9218a54" - integrity sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA== + version "0.4.4" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" + integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== dependencies: inline-style-parser "0.1.1" @@ -7472,19 +8084,12 @@ styled-jsx@5.0.7: resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.7.tgz#be44afc53771b983769ac654d355ca8d019dff48" integrity sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA== -styled-jsx@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" - integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== - dependencies: - client-only "0.0.1" - stylis@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== -sucrase@^3.32.0: +sucrase@^3.20.3, sucrase@^3.32.0: version "3.34.0" resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f" integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw== @@ -7523,10 +8128,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -swr@^2.1.3: - version "2.2.1" - resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.1.tgz#19b89a9034a62a73c30dbf06857a0a31981cd60a" - integrity sha512-KJVA7dGtOBeZ+2sycEuzUfVIP5lZ/cd0xjevv85n2YG0x1uHJQicjAtahVZL6xG3+TjqhbBqimwYzVo3saeVXQ== +swr@^2.1.3, swr@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07" + integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ== dependencies: client-only "^0.0.1" use-sync-external-store "^1.2.0" @@ -7548,24 +8153,24 @@ tailwind-merge@^1.14.0: integrity sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ== tailwindcss-animate@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.6.tgz#c7195037481552cc47962ea50113830360fd0c28" - integrity sha512-4WigSGMvbl3gCCact62ZvOngA+PRqhAn7si3TQ3/ZuPuQZcIEtVap+ENSXbzWhpojKB8CpvnIsrwBu8/RnHtuw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4" + integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== -tailwindcss@^3.1.6, tailwindcss@^3.2.7: - version "3.3.3" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf" - integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w== +tailwindcss@^3.2.7, tailwindcss@^3.3.3: + version "3.3.5" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.5.tgz#22a59e2fbe0ecb6660809d9cc5f3976b077be3b8" + integrity sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA== dependencies: "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" chokidar "^3.5.3" didyoumean "^1.2.2" dlv "^1.1.3" - fast-glob "^3.2.12" + fast-glob "^3.3.0" glob-parent "^6.0.2" is-glob "^4.0.3" - jiti "^1.18.2" + jiti "^1.19.1" lilconfig "^2.1.0" micromatch "^4.0.5" normalize-path "^3.0.0" @@ -7651,9 +8256,9 @@ terser-webpack-plugin@^5.3.3: terser "^5.16.8" terser@^5.0.0, terser@^5.16.8: - version "5.19.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" - integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== + version "5.24.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.24.0.tgz#4ae50302977bca4831ccc7b4fef63a3c04228364" + integrity sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -7702,20 +8307,15 @@ tippy.js@^6.3.7: "@popperjs/core" "^2.9.0" tiptap-markdown@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.2.tgz#93d42ef6225042d8bfe77861b93450aab184d67b" - integrity sha512-RyfpnH475+FYVh1fCiWF9+wBvA8T6X3PqxZR1MApEewxkoQ5kREQFiVwjZiez9qUQk/Bxu3RerFSJot65V6cbQ== + version "0.8.3" + resolved "https://registry.yarnpkg.com/tiptap-markdown/-/tiptap-markdown-0.8.3.tgz#51e93aad3c603c75a91111494ef4fd04115165fe" + integrity sha512-RULu1OXFTHdTJCHwdUOvDk0nDoH8YdgXulR0f+8XBd/x+SuT+EafdQuEKz2ggFlW1Mvl5niKGT5lAHTeXYFmaw== dependencies: "@types/markdown-it" "^12.2.3" markdown-it "^13.0.1" markdown-it-task-lists "^2.1.1" prosemirror-markdown "^1.11.1" -tlds@^1.238.0: - version "1.242.0" - resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.242.0.tgz#da136a9c95b0efa1a4cd57dca8ef240c08ada4b7" - integrity sha512-aP3dXawgmbfU94mA32CJGHmJUE1E58HCB1KmlKRhBNtqBL27mSQcAEmcaMaQ1Za9kIVvOdbxJD3U5ycDy7nJ3w== - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -7740,6 +8340,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" @@ -7770,16 +8375,56 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, "tslib@^2.4.1 || ^1.9.3": - version "2.6.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" - integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@~2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== tslib@~2.5.0: version "2.5.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== +tsup@^5.10.1: + version "5.12.9" + resolved "https://registry.yarnpkg.com/tsup/-/tsup-5.12.9.tgz#8cdd9b4bc6493317cb92edf5f3476920dddcdb18" + integrity sha512-dUpuouWZYe40lLufo64qEhDpIDsWhRbr2expv5dHEMjwqeKJS2aXA/FPqs1dxO4T6mBojo7rvo3jP9NNzaKyDg== + dependencies: + bundle-require "^3.0.2" + cac "^6.7.12" + chokidar "^3.5.1" + debug "^4.3.1" + esbuild "^0.14.25" + execa "^5.0.0" + globby "^11.0.3" + joycon "^3.0.1" + postcss-load-config "^3.0.1" + resolve-from "^5.0.0" + rollup "^2.74.1" + source-map "0.8.0-beta.0" + sucrase "^3.20.3" + tree-kill "^1.2.2" + +tsup@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/tsup/-/tsup-7.2.0.tgz#bb24c0d5e436477900c712e42adc67200607303c" + integrity sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ== + dependencies: + bundle-require "^4.0.0" + cac "^6.7.12" + chokidar "^3.5.1" + debug "^4.3.1" + esbuild "^0.18.2" + execa "^5.0.0" + globby "^11.0.3" + joycon "^3.0.1" + postcss-load-config "^4.0.1" + resolve-from "^5.0.0" + rollup "^3.2.5" + source-map "0.8.0-beta.0" + sucrase "^3.20.3" + tree-kill "^1.2.2" + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -7794,47 +8439,47 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -turbo-darwin-64@1.10.12: - version "1.10.12" - resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.10.12.tgz#a3d9d6bd3436e795254865bc3d76a9c79aff8085" - integrity sha512-vmDfGVPl5/aFenAbOj3eOx3ePNcWVUyZwYr7taRl0ZBbmv2TzjRiFotO4vrKCiTVnbqjQqAFQWY2ugbqCI1kOQ== - -turbo-darwin-arm64@1.10.12: - version "1.10.12" - resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.12.tgz#ff1d9466935687ca68c0dee88a909c34cc654357" - integrity sha512-3JliEESLNX2s7g54SOBqqkqJ7UhcOGkS0ywMr5SNuvF6kWVTbuUq7uBU/sVbGq8RwvK1ONlhPvJne5MUqBCTCQ== - -turbo-linux-64@1.10.12: - version "1.10.12" - resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.10.12.tgz#d184a491cc67c07672d339e36427378d0fc6b82e" - integrity sha512-siYhgeX0DidIfHSgCR95b8xPee9enKSOjCzx7EjTLmPqPaCiVebRYvbOIYdQWRqiaKh9yfhUtFmtMOMScUf1gg== - -turbo-linux-arm64@1.10.12: - version "1.10.12" - resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.10.12.tgz#44e6ca10b20fd4c59f8c85acf8366c7c09ebca81" - integrity sha512-K/ZhvD9l4SslclaMkTiIrnfcACgos79YcAo4kwc8bnMQaKuUeRpM15sxLpZp3xDjDg8EY93vsKyjaOhdFG2UbA== - -turbo-windows-64@1.10.12: - version "1.10.12" - resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.10.12.tgz#36380eca8e7b0505d08b87a084efab1500b2a80e" - integrity sha512-7FSgSwvktWDNOqV65l9AbZwcoueAILeE4L7JvjauNASAjjbuzXGCEq5uN8AQU3U5BOFj4TdXrVmO2dX+lLu8Zg== - -turbo-windows-arm64@1.10.12: - version "1.10.12" - resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.10.12.tgz#757ad59b9abf1949f22280bee6e8aad253743ba5" - integrity sha512-gCNXF52dwom1HLY9ry/cneBPOKTBHhzpqhMylcyvJP0vp9zeMQQkt6yjYv+6QdnmELC92CtKNp2FsNZo+z0pyw== - -turbo@latest: - version "1.10.12" - resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.10.12.tgz#cd6f56b92da0600dae9fd94230483556a5c83187" - integrity sha512-WM3+jTfQWnB9W208pmP4oeehZcC6JQNlydb/ZHMRrhmQa+htGhWLCzd6Q9rLe0MwZLPpSPFV2/bN5egCLyoKjQ== +turbo-darwin-64@1.10.16: + version "1.10.16" + resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.10.16.tgz#5a8717c1372f2a75e8cfe0b4b6455119ce410b19" + integrity sha512-+Jk91FNcp9e9NCLYlvDDlp2HwEDp14F9N42IoW3dmHI5ZkGSXzalbhVcrx3DOox3QfiNUHxzWg4d7CnVNCuuMg== + +turbo-darwin-arm64@1.10.16: + version "1.10.16" + resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.16.tgz#19b5b63acf7ee0fce7e1fe5850e093c2ac9c73f5" + integrity sha512-jqGpFZipIivkRp/i+jnL8npX0VssE6IAVNKtu573LXtssZdV/S+fRGYA16tI46xJGxSAivrZ/IcgZrV6Jk80bw== + +turbo-linux-64@1.10.16: + version "1.10.16" + resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.10.16.tgz#ee97c0011553a96bd7995e7bcc6e65aab8996798" + integrity sha512-PpqEZHwLoizQ6sTUvmImcRmACyRk9EWLXGlqceogPZsJ1jTRK3sfcF9fC2W56zkSIzuLEP07k5kl+ZxJd8JMcg== + +turbo-linux-arm64@1.10.16: + version "1.10.16" + resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.10.16.tgz#2723cc2a846d89df7484002161b25673f4f04009" + integrity sha512-TMjFYz8to1QE0fKVXCIvG/4giyfnmqcQIwjdNfJvKjBxn22PpbjeuFuQ5kNXshUTRaTJihFbuuCcb5OYFNx4uw== + +turbo-windows-64@1.10.16: + version "1.10.16" + resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.10.16.tgz#87c46a78502a86dec73634b255a6b3471285969b" + integrity sha512-+jsf68krs0N66FfC4/zZvioUap/Tq3sPFumnMV+EBo8jFdqs4yehd6+MxIwYTjSQLIcpH8KoNMB0gQYhJRLZzw== + +turbo-windows-arm64@1.10.16: + version "1.10.16" + resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.10.16.tgz#a6208c2bc27e5e6ef0aa4a3e64389c4285c3f274" + integrity sha512-sKm3hcMM1bl0B3PLG4ifidicOGfoJmOEacM5JtgBkYM48ncMHjkHfFY7HrJHZHUnXM4l05RQTpLFoOl/uIo2HQ== + +turbo@^1.10.16: + version "1.10.16" + resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.10.16.tgz#51601a65a3aa56d1b9d9cb9a2ab3a5a2eab41e19" + integrity sha512-2CEaK4FIuSZiP83iFa9GqMTQhroW2QryckVqUydmg4tx78baftTOS0O+oDAhvo9r9Nit4xUEtC1RAHoqs6ZEtg== optionalDependencies: - turbo-darwin-64 "1.10.12" - turbo-darwin-arm64 "1.10.12" - turbo-linux-64 "1.10.12" - turbo-linux-arm64 "1.10.12" - turbo-windows-64 "1.10.12" - turbo-windows-arm64 "1.10.12" + turbo-darwin-64 "1.10.16" + turbo-darwin-arm64 "1.10.16" + turbo-linux-64 "1.10.16" + turbo-linux-arm64 "1.10.16" + turbo-windows-64 "1.10.16" + turbo-windows-arm64 "1.10.16" type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -7932,6 +8577,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -8019,19 +8669,19 @@ unist-util-visit@^4.0.0: unist-util-visit-parents "^5.1.1" universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== upath@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -8057,6 +8707,15 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-loader@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" + integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== + dependencies: + loader-utils "^2.0.0" + mime-types "^2.1.27" + schema-utils "^3.0.0" + use-callback-ref@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" @@ -8069,7 +8728,7 @@ use-debounce@^9.0.4: resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.4.tgz#51d25d856fbdfeb537553972ce3943b897f1ac85" integrity sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ== -use-memo-one@^1.1.1: +use-memo-one@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== @@ -8082,7 +8741,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -8092,15 +8751,10 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - uuid@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== uvu@^0.5.0: version "0.5.6" @@ -8113,9 +8767,9 @@ uvu@^0.5.0: sade "^1.7.3" v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + version "2.4.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" + integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== vfile-message@^3.0.0: version "3.1.4" @@ -8147,14 +8801,6 @@ warning@^4.0.2, warning@^4.0.3: dependencies: loose-envify "^1.0.0" -watchpack@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -8206,13 +8852,41 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" -which-typed-array@^1.1.10, which-typed-array@^1.1.11: - version "1.1.11" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" - integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + +which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.9: + version "1.1.13" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== dependencies: available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + call-bind "^1.0.4" for-each "^0.3.3" gopd "^1.0.1" has-tostringtag "^1.0.0" @@ -8408,22 +9082,17 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0: +yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yaml@^2.1.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" - integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zod@3.21.4: - version "3.21.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" - integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==