Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf696d200d | ||
|
|
bb128e3e16 | ||
|
|
63fac3b8c4 | ||
|
|
587fe76032 | ||
|
|
a18d90da86 | ||
|
|
febf98ea54 | ||
|
|
5747dc6fd8 | ||
|
|
d83944cc8d | ||
|
|
799b9cbfc5 | ||
|
|
a01b51fca5 | ||
|
|
00a51f5e6a | ||
|
|
b73d6344ad | ||
|
|
f0ec84661d | ||
|
|
d8ed19f204 | ||
|
|
9fa707b260 | ||
|
|
d7c80885fd | ||
|
|
9851fe0b8f | ||
|
|
5e237938ff | ||
|
|
f0468a9173 | ||
|
|
c53968a7f8 | ||
|
|
97b4abd693 | ||
|
|
130ba5ee6c | ||
|
|
113bba46ea | ||
|
|
ce401c723e | ||
|
|
5396d438a3 | ||
|
|
942d2b98ef | ||
|
|
d94a269451 | ||
|
|
54b80e91eb | ||
|
|
6e033f9fdb | ||
|
|
f3c7c057b4 | ||
|
|
d91b5a274b | ||
|
|
5a7d1ebd65 | ||
|
|
04d4490293 | ||
|
|
d9695afcdc | ||
|
|
c3c7c72aff | ||
|
|
9d3b5d9da7 | ||
|
|
1faf06c755 | ||
|
|
72b6453f6f | ||
|
|
428cb478b1 | ||
|
|
e972989522 | ||
|
|
588dc2927e | ||
|
|
6627282bc5 | ||
|
|
d7c12f9730 | ||
|
|
7902805635 | ||
|
|
7b1f5a47f5 | ||
|
|
9a7696acac | ||
|
|
2a978e3ac0 | ||
|
|
8c23fdd1d8 | ||
|
|
a77af4e67e | ||
|
|
b783f25bfa | ||
|
|
95d121ce38 | ||
|
|
318c993082 | ||
|
|
6c984e18ae | ||
|
|
ec44b63027 | ||
|
|
1548288e95 | ||
|
|
81cea3256a | ||
|
|
07f269e7f3 | ||
|
|
ce69644d53 |
@@ -1,7 +0,0 @@
|
||||
[codespell]
|
||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
||||
skip = .git*,*.svg,i18n,*-lock.yaml,*.css,.codespellrc,migrations,*.js,*.map,*.mjs
|
||||
check-hidden = true
|
||||
# ignore all CamelCase and camelCase
|
||||
ignore-regex = \b[A-Za-z][a-z]+[A-Z][a-zA-Z]+\b
|
||||
ignore-words-list = tread
|
||||
@@ -1,55 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# JavaScript/TypeScript dependencies (pnpm monorepo root)
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "javascript"
|
||||
groups:
|
||||
minor-and-patch:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# Python dependencies
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/apps/api"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "python"
|
||||
groups:
|
||||
minor-and-patch:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
# Docker - API
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/apps/api"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "docker"
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
branch_build_push_admin:
|
||||
name: Build-Push Admin Docker Image
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- name: Admin Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
uses: makeplane/actions/build-push@v1.4.0
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- name: Web Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
uses: makeplane/actions/build-push@v1.4.0
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- name: Space Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
uses: makeplane/actions/build-push@v1.4.0
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- name: Live Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
uses: makeplane/actions/build-push@v1.4.0
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- name: Backend Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
uses: makeplane/actions/build-push@v1.4.0
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -252,7 +252,7 @@ jobs:
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- name: Proxy Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
uses: makeplane/actions/build-push@v1.4.0
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
- branch_build_push_proxy
|
||||
steps:
|
||||
- name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare AIO Assets
|
||||
id: prepare_aio_assets
|
||||
@@ -298,13 +298,13 @@ jobs:
|
||||
echo "AIO_BUILD_VERSION=${aio_version}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload AIO Assets
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ./deployments/aio/community/dist
|
||||
name: aio-assets-dist
|
||||
|
||||
- name: AIO Build and Push
|
||||
uses: makeplane/actions/build-push@v1.1.0
|
||||
uses: makeplane/actions/build-push@v1.4.0
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -337,7 +337,7 @@ jobs:
|
||||
- branch_build_push_proxy
|
||||
steps:
|
||||
- name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Update Assets
|
||||
run: |
|
||||
@@ -352,7 +352,7 @@ jobs:
|
||||
# sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deployments/cli/community/variables.env
|
||||
|
||||
- name: Upload Assets
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: community-assets
|
||||
path: |
|
||||
@@ -381,7 +381,7 @@ jobs:
|
||||
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Update Assets
|
||||
run: |
|
||||
@@ -391,12 +391,13 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2.1.0
|
||||
uses: softprops/action-gh-release@v2.6.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
with:
|
||||
tag_name: ${{ env.REL_VERSION }}
|
||||
name: ${{ env.REL_VERSION }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
draft: false
|
||||
prerelease: ${{ env.IS_PRERELEASE }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -10,13 +10,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
- name: Get PR Branch version
|
||||
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Codespell configuration is within .codespellrc
|
||||
---
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [preview]
|
||||
pull_request:
|
||||
branches: [preview]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Annotate locations with typos
|
||||
uses: codespell-project/codespell-problem-matcher@v1
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.22"
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
full_build_push:
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -63,23 +63,23 @@ jobs:
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v7.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
sudo apt-get install -y python3-pip
|
||||
pip3 install awscli
|
||||
- name: Tailscale
|
||||
uses: tailscale/github-action@v2
|
||||
uses: tailscale/github-action@v4
|
||||
with:
|
||||
oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
|
||||
oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
|
||||
|
||||
@@ -8,8 +8,6 @@ on:
|
||||
types:
|
||||
- "opened"
|
||||
- "synchronize"
|
||||
- "ready_for_review"
|
||||
- "review_requested"
|
||||
- "reopened"
|
||||
|
||||
concurrency:
|
||||
@@ -46,7 +44,7 @@ jobs:
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
@@ -89,7 +87,7 @@ jobs:
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
@@ -97,7 +95,7 @@ jobs:
|
||||
pnpm-store-${{ runner.os }}-
|
||||
|
||||
- name: Restore Turbo cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
|
||||
@@ -112,7 +110,7 @@ jobs:
|
||||
run: pnpm turbo run build --affected
|
||||
|
||||
- name: Save Turbo cache
|
||||
uses: actions/cache/save@v4
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
|
||||
@@ -146,7 +144,7 @@ jobs:
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
@@ -187,7 +185,7 @@ jobs:
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
@@ -195,7 +193,7 @@ jobs:
|
||||
pnpm-store-${{ runner.os }}-
|
||||
|
||||
- name: Restore Turbo cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Create PR on Sync
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "sync/**"
|
||||
|
||||
env:
|
||||
CURRENT_BRANCH: ${{ github.ref_name }}
|
||||
TARGET_BRANCH: "preview" # The target branch that you would like to merge changes like develop
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
|
||||
ACCOUNT_USER_NAME: ${{ vars.ACCOUNT_USER_NAME }}
|
||||
ACCOUNT_USER_EMAIL: ${{ vars.ACCOUNT_USER_EMAIL }}
|
||||
|
||||
jobs:
|
||||
create_pull_request:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for all branches and tags
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config user.name "$ACCOUNT_USER_NAME"
|
||||
git config user.email "$ACCOUNT_USER_EMAIL"
|
||||
|
||||
- name: Setup GH CLI and Git Config
|
||||
run: |
|
||||
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||
sudo apt update
|
||||
sudo apt install gh -y
|
||||
|
||||
- name: Create PR to Target Branch
|
||||
run: |
|
||||
# get all pull requests and check if there is already a PR
|
||||
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $CURRENT_BRANCH --state open --json number | jq '.[] | .number')
|
||||
if [ -n "$PR_EXISTS" ]; then
|
||||
echo "Pull Request already exists: $PR_EXISTS"
|
||||
else
|
||||
echo "Creating new pull request"
|
||||
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $CURRENT_BRANCH --title "${{ vars.SYNC_PR_TITLE }}" --body "")
|
||||
echo "Pull Request created: $PR_URL"
|
||||
fi
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Sync Repositories
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- preview
|
||||
|
||||
env:
|
||||
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
sync_changes:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup GH CLI
|
||||
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: Push Changes to Target Repo
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
run: |
|
||||
TARGET_REPO="${{ vars.SYNC_TARGET_REPO }}"
|
||||
TARGET_BRANCH="${{ vars.SYNC_TARGET_BRANCH_NAME }}"
|
||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||
|
||||
git checkout $SOURCE_BRANCH
|
||||
git remote add target-origin-a "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target-origin-a $SOURCE_BRANCH:$TARGET_BRANCH
|
||||
@@ -13,7 +13,7 @@ RUN corepack enable pnpm
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
RUN pnpm add -g turbo@2.8.12
|
||||
RUN pnpm add -g turbo@2.9.4
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ export function HydrateFallback() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
export function ErrorBoundary({ error: _error }: Route.ErrorBoundaryProps) {
|
||||
return (
|
||||
<div>
|
||||
<p>Something went wrong.</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"description": "Admin UI for Plane",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -49,7 +49,6 @@
|
||||
"uuid": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "catalog:",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@react-router/dev": "catalog:",
|
||||
@@ -57,6 +56,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "node:path";
|
||||
import * as dotenv from "@dotenvx/dotenvx";
|
||||
import * as dotenv from "dotenv";
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "1.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
"description": "API server powering Plane's backend",
|
||||
"license": "AGPL-3.0"
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ from .issue import (
|
||||
IssueCommentCreateSerializer,
|
||||
IssueLinkCreateSerializer,
|
||||
IssueLinkUpdateSerializer,
|
||||
IssueRelationCreateSerializer,
|
||||
IssueRelationResponseSerializer,
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
)
|
||||
from .state import StateLiteSerializer, StateSerializer
|
||||
from .cycle import (
|
||||
@@ -49,7 +53,7 @@ from .intake import (
|
||||
IntakeIssueCreateSerializer,
|
||||
IntakeIssueUpdateSerializer,
|
||||
)
|
||||
from .estimate import EstimatePointSerializer
|
||||
from .estimate import EstimateSerializer, EstimatePointSerializer
|
||||
from .asset import (
|
||||
UserAssetUploadSerializer,
|
||||
AssetUpdateSerializer,
|
||||
|
||||
@@ -2,20 +2,36 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import EstimatePoint
|
||||
from plane.db.models import Estimate, EstimatePoint
|
||||
from .base import BaseSerializer
|
||||
|
||||
|
||||
class EstimatePointSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for project estimation points and story point values.
|
||||
class EstimateSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Estimate
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "project", "deleted_at"]
|
||||
|
||||
Handles numeric estimation data for work item sizing and sprint planning,
|
||||
providing standardized point values for project velocity calculations.
|
||||
"""
|
||||
def create(self, validated_data):
|
||||
validated_data["workspace"] = self.context["workspace"]
|
||||
validated_data["project"] = self.context["project"]
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class EstimatePointSerializer(BaseSerializer):
|
||||
def validate(self, data):
|
||||
if not data:
|
||||
raise serializers.ValidationError("Estimate points are required")
|
||||
value = data.get("value")
|
||||
if value and len(value) > 20:
|
||||
raise serializers.ValidationError("Value can't be more than 20 characters")
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
model = EstimatePoint
|
||||
fields = ["id", "value"]
|
||||
read_only_fields = fields
|
||||
fields = "__all__"
|
||||
read_only_fields = ["estimate", "workspace", "project"]
|
||||
|
||||
@@ -20,6 +20,7 @@ from plane.db.models import (
|
||||
IssueComment,
|
||||
IssueLabel,
|
||||
IssueLink,
|
||||
IssueRelation,
|
||||
Label,
|
||||
ProjectMember,
|
||||
State,
|
||||
@@ -479,6 +480,184 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class IssueRelationResponseSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for issue relations response showing grouped relation types.
|
||||
|
||||
Returns issue IDs organized by relation type for efficient client-side processing.
|
||||
"""
|
||||
|
||||
blocking = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that are blocking this issue",
|
||||
)
|
||||
blocked_by = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that this issue is blocked by",
|
||||
)
|
||||
duplicate = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that are duplicates of this issue",
|
||||
)
|
||||
relates_to = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that relate to this issue",
|
||||
)
|
||||
start_after = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that start after this issue",
|
||||
)
|
||||
start_before = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that start before this issue",
|
||||
)
|
||||
finish_after = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that finish after this issue",
|
||||
)
|
||||
finish_before = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that finish before this issue",
|
||||
)
|
||||
|
||||
|
||||
class IssueRelationCreateSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for creating issue relations.
|
||||
|
||||
Creates issue relations with the specified relation type and issues.
|
||||
Validates relation types and ensures proper issue ID format.
|
||||
"""
|
||||
|
||||
RELATION_TYPE_CHOICES = [
|
||||
("blocking", "Blocking"),
|
||||
("blocked_by", "Blocked By"),
|
||||
("duplicate", "Duplicate"),
|
||||
("relates_to", "Relates To"),
|
||||
("start_before", "Start Before"),
|
||||
("start_after", "Start After"),
|
||||
("finish_before", "Finish Before"),
|
||||
("finish_after", "Finish After"),
|
||||
]
|
||||
|
||||
relation_type = serializers.ChoiceField(
|
||||
choices=RELATION_TYPE_CHOICES,
|
||||
required=True,
|
||||
help_text="Type of relationship between work items",
|
||||
)
|
||||
issues = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=True,
|
||||
min_length=1,
|
||||
help_text="Array of work item IDs to create relations with",
|
||||
)
|
||||
|
||||
def validate_issues(self, value):
|
||||
"""Validate that issues list is not empty and contains valid UUIDs."""
|
||||
if not value:
|
||||
raise serializers.ValidationError("At least one issue ID is required.")
|
||||
return value
|
||||
|
||||
|
||||
class IssueRelationRemoveSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for removing issue relations.
|
||||
|
||||
Removes existing relationships between work items by specifying
|
||||
the related issue ID.
|
||||
"""
|
||||
|
||||
related_issue = serializers.UUIDField(
|
||||
required=True, help_text="ID of the related work item to remove relation with"
|
||||
)
|
||||
|
||||
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for issue relationships showing related issue details.
|
||||
|
||||
Provides comprehensive information about related issues including
|
||||
project context, sequence ID, and relationship type.
|
||||
"""
|
||||
|
||||
id = serializers.UUIDField(source="related_issue.id", read_only=True)
|
||||
project_id = serializers.UUIDField(source="related_issue.project_id", read_only=True)
|
||||
sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="related_issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="related_issue.priority", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"state_id",
|
||||
"priority",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for reverse issue relationships showing issue details.
|
||||
|
||||
Provides comprehensive information about the source issue in a relationship
|
||||
including project context, sequence ID, and relationship type.
|
||||
"""
|
||||
|
||||
id = serializers.UUIDField(source="issue.id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True)
|
||||
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="issue.name", read_only=True)
|
||||
type_id = serializers.UUIDField(source="issue.type.id", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
is_epic = serializers.BooleanField(source="issue.type.is_epic", read_only=True)
|
||||
state_id = serializers.UUIDField(source="issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="issue.priority", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"type_id",
|
||||
"is_epic",
|
||||
"state_id",
|
||||
"priority",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class IssueAttachmentSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for work item file attachments.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views.estimate import (
|
||||
ProjectEstimateAPIEndpoint,
|
||||
EstimatePointListCreateAPIEndpoint,
|
||||
EstimatePointDetailAPIEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||
ProjectEstimateAPIEndpoint.as_view(http_method_names=["get", "post", "patch", "delete"]),
|
||||
name="project-estimate",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
|
||||
EstimatePointListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="estimate-point-list-create",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<uuid:estimate_point_id>/",
|
||||
EstimatePointDetailAPIEndpoint.as_view(http_method_names=["patch", "delete"]),
|
||||
name="estimate-point-detail",
|
||||
),
|
||||
]
|
||||
@@ -17,6 +17,7 @@ from plane.api.views import (
|
||||
IssueAttachmentDetailAPIEndpoint,
|
||||
WorkspaceIssueAPIEndpoint,
|
||||
IssueSearchEndpoint,
|
||||
IssueRelationListCreateAPIEndpoint,
|
||||
)
|
||||
|
||||
# Deprecated url patterns
|
||||
@@ -145,6 +146,11 @@ new_url_patterns = [
|
||||
IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="work-item-attachment-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/relations/",
|
||||
IssueRelationListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="work-item-relation-list",
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns = old_url_patterns + new_url_patterns
|
||||
|
||||
@@ -29,6 +29,7 @@ from .issue import (
|
||||
IssueAttachmentListCreateAPIEndpoint,
|
||||
IssueAttachmentDetailAPIEndpoint,
|
||||
IssueSearchEndpoint,
|
||||
IssueRelationListCreateAPIEndpoint,
|
||||
)
|
||||
|
||||
from .cycle import (
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from drf_spectacular.utils import OpenApiRequest, OpenApiResponse
|
||||
|
||||
# Module imports
|
||||
from plane.app.permissions.project import ProjectEntityPermission
|
||||
from plane.api.views.base import BaseAPIView
|
||||
from plane.db.models import Estimate, EstimatePoint, Project, Workspace
|
||||
from plane.api.serializers import EstimateSerializer, EstimatePointSerializer
|
||||
from plane.utils.openapi.decorators import estimate_docs, estimate_point_docs
|
||||
from plane.utils.openapi import (
|
||||
ESTIMATE_CREATE_EXAMPLE,
|
||||
ESTIMATE_UPDATE_EXAMPLE,
|
||||
ESTIMATE_POINT_CREATE_EXAMPLE,
|
||||
ESTIMATE_POINT_UPDATE_EXAMPLE,
|
||||
ESTIMATE_EXAMPLE,
|
||||
ESTIMATE_POINT_EXAMPLE,
|
||||
DELETED_RESPONSE,
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
ESTIMATE_ID_PARAMETER,
|
||||
)
|
||||
|
||||
|
||||
class ProjectEstimateAPIEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
model = Estimate
|
||||
serializer_class = EstimateSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(workspace__slug=self.workspace_slug, project_id=self.project_id)
|
||||
|
||||
@estimate_docs(
|
||||
operation_id="create_estimate",
|
||||
summary="Create an estimate",
|
||||
description="Create an estimate for a project",
|
||||
request=OpenApiRequest(
|
||||
request=EstimateSerializer,
|
||||
examples=[ESTIMATE_CREATE_EXAMPLE],
|
||||
),
|
||||
)
|
||||
def post(self, request, slug, project_id):
|
||||
project = Project.objects.filter(id=project_id, workspace__slug=slug).first()
|
||||
if not project:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Project not found"})
|
||||
|
||||
workspace = Workspace.objects.filter(slug=slug).first()
|
||||
if not workspace:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Workspace not found"})
|
||||
|
||||
project_estimate = self.get_queryset().first()
|
||||
if project_estimate:
|
||||
# return 409 if the project estimate already exists
|
||||
return Response(
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
data={"error": "An estimate already exists for this project", "id": str(project_estimate.id)},
|
||||
)
|
||||
# create the project estimate
|
||||
serializer = self.serializer_class(data=request.data, context={"workspace": workspace, "project": project})
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@estimate_docs(
|
||||
operation_id="get_estimate",
|
||||
summary="Get an estimate",
|
||||
description="Get an estimate for a project",
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="Estimate",
|
||||
response=EstimateSerializer,
|
||||
examples=[ESTIMATE_EXAMPLE],
|
||||
),
|
||||
},
|
||||
)
|
||||
def get(self, request, slug, project_id):
|
||||
estimate = self.get_queryset().first()
|
||||
if not estimate:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||
serializer = self.serializer_class(estimate)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@estimate_docs(
|
||||
operation_id="update_estimate",
|
||||
summary="Update an estimate",
|
||||
description="Update an estimate for a project",
|
||||
request=OpenApiRequest(
|
||||
request=EstimateSerializer,
|
||||
examples=[ESTIMATE_UPDATE_EXAMPLE],
|
||||
),
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="Estimate",
|
||||
response=EstimateSerializer,
|
||||
examples=[ESTIMATE_EXAMPLE],
|
||||
),
|
||||
},
|
||||
)
|
||||
def patch(self, request, slug, project_id):
|
||||
ALLOWED_FIELDS = ["name", "description"]
|
||||
estimate = self.get_queryset().first()
|
||||
if not estimate:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||
filtered_data = {k: v for k, v in request.data.items() if k in ALLOWED_FIELDS}
|
||||
if not filtered_data:
|
||||
serializer = self.serializer_class(estimate)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
serializer = self.serializer_class(estimate, data=filtered_data, partial=True)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@estimate_docs(
|
||||
operation_id="delete_estimate",
|
||||
summary="Delete an estimate",
|
||||
description="Delete an estimate for a project",
|
||||
responses={
|
||||
204: DELETED_RESPONSE,
|
||||
},
|
||||
)
|
||||
def delete(self, request, slug, project_id):
|
||||
estimate = self.get_queryset().first()
|
||||
if not estimate:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||
estimate.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class EstimatePointListCreateAPIEndpoint(BaseAPIView):
|
||||
"""List and bulk create estimate points for an estimate."""
|
||||
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
model = EstimatePoint
|
||||
serializer_class = EstimatePointSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(
|
||||
estimate_id=self.kwargs["estimate_id"],
|
||||
workspace__slug=self.kwargs["slug"],
|
||||
project_id=self.kwargs["project_id"],
|
||||
).select_related("estimate", "workspace", "project")
|
||||
|
||||
@estimate_point_docs(
|
||||
operation_id="get_estimate_points",
|
||||
summary="Get estimate points",
|
||||
description="Get estimate points for an estimate",
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
ESTIMATE_ID_PARAMETER,
|
||||
],
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="Estimate points",
|
||||
response=EstimatePointSerializer(many=True),
|
||||
examples=[ESTIMATE_POINT_EXAMPLE],
|
||||
),
|
||||
},
|
||||
)
|
||||
def get(self, request, slug, project_id, estimate_id):
|
||||
estimate = Estimate.objects.filter(
|
||||
id=estimate_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
).first()
|
||||
if not estimate:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||
estimate_points = self.get_queryset()
|
||||
serializer = self.serializer_class(estimate_points, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@estimate_point_docs(
|
||||
operation_id="create_estimate_points",
|
||||
summary="Create estimate points",
|
||||
description="Create estimate points for an estimate",
|
||||
request=OpenApiRequest(
|
||||
request=EstimatePointSerializer,
|
||||
examples=[ESTIMATE_POINT_CREATE_EXAMPLE],
|
||||
),
|
||||
responses={
|
||||
201: OpenApiResponse(
|
||||
description="Estimate points",
|
||||
response=EstimatePointSerializer(many=True),
|
||||
examples=[ESTIMATE_POINT_EXAMPLE],
|
||||
),
|
||||
},
|
||||
)
|
||||
def post(self, request, slug, project_id, estimate_id):
|
||||
estimate = Estimate.objects.filter(
|
||||
id=estimate_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
).first()
|
||||
if not estimate:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||
|
||||
estimate_points_data = (
|
||||
request.data if isinstance(request.data, list) else request.data.get("estimate_points", [])
|
||||
)
|
||||
if not estimate_points_data:
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={"error": "Estimate points are required"},
|
||||
)
|
||||
|
||||
serializer = self.serializer_class(data=estimate_points_data, many=True)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
estimate_points = [
|
||||
EstimatePoint(
|
||||
estimate=estimate,
|
||||
workspace=estimate.workspace,
|
||||
project=estimate.project,
|
||||
**item,
|
||||
)
|
||||
for item in serializer.validated_data
|
||||
]
|
||||
created = EstimatePoint.objects.bulk_create(estimate_points)
|
||||
return Response(
|
||||
self.serializer_class(created, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class EstimatePointDetailAPIEndpoint(BaseAPIView):
|
||||
"""Update and delete a single estimate point."""
|
||||
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
model = EstimatePoint
|
||||
serializer_class = EstimatePointSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(
|
||||
estimate_id=self.kwargs["estimate_id"],
|
||||
workspace__slug=self.kwargs["slug"],
|
||||
project_id=self.kwargs["project_id"],
|
||||
)
|
||||
|
||||
@estimate_point_docs(
|
||||
operation_id="update_estimate_point",
|
||||
summary="Update an estimate point",
|
||||
description="Update an estimate point for an estimate",
|
||||
request=OpenApiRequest(
|
||||
request=EstimatePointSerializer,
|
||||
examples=[ESTIMATE_POINT_UPDATE_EXAMPLE],
|
||||
),
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="Estimate point",
|
||||
response=EstimatePointSerializer,
|
||||
examples=[ESTIMATE_POINT_EXAMPLE],
|
||||
),
|
||||
},
|
||||
)
|
||||
def patch(self, request, slug, project_id, estimate_id, estimate_point_id):
|
||||
estimate_point = self.get_queryset().filter(id=estimate_point_id).first()
|
||||
if not estimate_point:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate point not found"})
|
||||
ALLOWED_FIELDS = ["key", "value", "description"]
|
||||
filtered_data = {k: v for k, v in request.data.items() if k in ALLOWED_FIELDS}
|
||||
if not filtered_data:
|
||||
return Response(self.serializer_class(estimate_point).data, status=status.HTTP_200_OK)
|
||||
serializer = self.serializer_class(estimate_point, data=filtered_data, partial=True)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@estimate_point_docs(
|
||||
operation_id="delete_estimate_point",
|
||||
summary="Delete an estimate point",
|
||||
description="Delete an estimate point for an estimate",
|
||||
responses={
|
||||
204: DELETED_RESPONSE,
|
||||
},
|
||||
)
|
||||
def delete(self, request, slug, project_id, estimate_id, estimate_point_id):
|
||||
estimate_point = self.get_queryset().filter(id=estimate_point_id).first()
|
||||
if not estimate_point:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate point not found"})
|
||||
estimate_point.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -23,7 +23,11 @@ from django.db.models import (
|
||||
Value,
|
||||
When,
|
||||
Subquery,
|
||||
UUIDField,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
@@ -45,6 +49,9 @@ from plane.api.serializers import (
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueLinkSerializer,
|
||||
IssueRelationCreateSerializer,
|
||||
IssueRelationResponseSerializer,
|
||||
IssueRelationSerializer,
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
IssueAttachmentUploadSerializer,
|
||||
@@ -53,6 +60,7 @@ from plane.api.serializers import (
|
||||
IssueLinkCreateSerializer,
|
||||
IssueLinkUpdateSerializer,
|
||||
LabelCreateUpdateSerializer,
|
||||
RelatedIssueSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
@@ -66,6 +74,7 @@ from plane.db.models import (
|
||||
FileAsset,
|
||||
IssueComment,
|
||||
IssueLink,
|
||||
IssueRelation,
|
||||
Label,
|
||||
Project,
|
||||
ProjectMember,
|
||||
@@ -76,10 +85,12 @@ from plane.settings.storage import S3Storage
|
||||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.issue_relation_mapper import get_actual_relation
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.app.permissions import ROLE
|
||||
from plane.utils.openapi import (
|
||||
work_item_docs,
|
||||
work_item_relation_docs,
|
||||
label_docs,
|
||||
issue_link_docs,
|
||||
issue_comment_docs,
|
||||
@@ -629,6 +640,16 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
# Send the model activity for webhook dispatch
|
||||
model_activity.delay(
|
||||
model_name="issue",
|
||||
model_id=str(issue.id),
|
||||
requested_data=request.data,
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
# If the serializer is not valid, respond with 400 bad
|
||||
@@ -677,6 +698,16 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
# Send the model activity for webhook dispatch
|
||||
model_activity.delay(
|
||||
model_name="issue",
|
||||
model_id=str(serializer.data["id"]),
|
||||
requested_data=request.data,
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
@@ -752,6 +783,16 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
# Send the model activity for webhook dispatch
|
||||
model_activity.delay(
|
||||
model_name="issue",
|
||||
model_id=str(pk),
|
||||
requested_data=request.data,
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -1089,9 +1130,9 @@ class IssueLinkListCreateAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_links: IssueLinkSerializer(
|
||||
issue_links, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issue_links: (
|
||||
IssueLinkSerializer(issue_links, many=True, fields=self.fields, expand=self.expand).data
|
||||
),
|
||||
)
|
||||
|
||||
@issue_link_docs(
|
||||
@@ -1196,9 +1237,9 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_links: IssueLinkSerializer(
|
||||
issue_links, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issue_links: (
|
||||
IssueLinkSerializer(issue_links, many=True, fields=self.fields, expand=self.expand).data
|
||||
),
|
||||
)
|
||||
issue_link = self.get_queryset().get(pk=pk)
|
||||
serializer = IssueLinkSerializer(issue_link, fields=self.fields, expand=self.expand)
|
||||
@@ -1347,9 +1388,9 @@ class IssueCommentListCreateAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_comments: IssueCommentSerializer(
|
||||
issue_comments, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issue_comments: (
|
||||
IssueCommentSerializer(issue_comments, many=True, fields=self.fields, expand=self.expand).data
|
||||
),
|
||||
)
|
||||
|
||||
@issue_comment_docs(
|
||||
@@ -1658,9 +1699,9 @@ class IssueActivityListAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issue_activities),
|
||||
on_results=lambda issue_activity: IssueActivitySerializer(
|
||||
issue_activity, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issue_activity: (
|
||||
IssueActivitySerializer(issue_activity, many=True, fields=self.fields, expand=self.expand).data
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -2220,3 +2261,224 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
)[: int(limit)]
|
||||
|
||||
return Response({"issues": issue_results}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueRelationListCreateAPIEndpoint(BaseAPIView):
|
||||
"""Issue Relation List and Create Endpoint"""
|
||||
|
||||
serializer_class = IssueRelationSerializer
|
||||
model = IssueRelation
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
use_read_replica = True
|
||||
|
||||
@work_item_relation_docs(
|
||||
operation_id="list_work_item_relations",
|
||||
summary="List work item relations",
|
||||
description="Retrieve all relationships for a work item including blocking, blocked_by, duplicate, relates_to, start_before, start_after, finish_before, and finish_after relations.", # noqa E501
|
||||
parameters=[
|
||||
ISSUE_ID_PARAMETER,
|
||||
CURSOR_PARAMETER,
|
||||
PER_PAGE_PARAMETER,
|
||||
ORDER_BY_PARAMETER,
|
||||
FIELDS_PARAMETER,
|
||||
EXPAND_PARAMETER,
|
||||
],
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="Work item relations grouped by relation type",
|
||||
response=IssueRelationResponseSerializer,
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name="Work Item Relations Response",
|
||||
value={
|
||||
"blocking": [
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"550e8400-e29b-41d4-a716-446655440001",
|
||||
],
|
||||
"blocked_by": ["550e8400-e29b-41d4-a716-446655440002"],
|
||||
"duplicate": [],
|
||||
"relates_to": ["550e8400-e29b-41d4-a716-446655440003"],
|
||||
"start_after": [],
|
||||
"start_before": ["550e8400-e29b-41d4-a716-446655440004"],
|
||||
"finish_after": [],
|
||||
"finish_before": [],
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
400: INVALID_REQUEST_RESPONSE,
|
||||
404: ISSUE_NOT_FOUND_RESPONSE,
|
||||
},
|
||||
)
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
"""List work item relations
|
||||
|
||||
Retrieve all relationships for a work item organized by relation type.
|
||||
Returns a structured response with relations grouped by type.
|
||||
"""
|
||||
empty_uuid_array = Value([], output_field=ArrayField(UUIDField()))
|
||||
|
||||
def _agg_ids(field, **filter_kwargs):
|
||||
return Coalesce(
|
||||
ArrayAgg(field, filter=Q(**filter_kwargs), distinct=True),
|
||||
empty_uuid_array,
|
||||
)
|
||||
|
||||
issue_relation_qs = IssueRelation.objects.filter(
|
||||
Q(issue_id=issue_id) | Q(related_issue_id=issue_id),
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
relation_ids = issue_relation_qs.aggregate(
|
||||
blocking_ids=_agg_ids("issue_id", relation_type="blocked_by", related_issue_id=issue_id),
|
||||
blocked_by_ids=_agg_ids("related_issue_id", relation_type="blocked_by", issue_id=issue_id),
|
||||
duplicate_ids=_agg_ids("related_issue_id", relation_type="duplicate", issue_id=issue_id),
|
||||
duplicate_ids_related=_agg_ids("issue_id", relation_type="duplicate", related_issue_id=issue_id),
|
||||
relates_to_ids=_agg_ids("related_issue_id", relation_type="relates_to", issue_id=issue_id),
|
||||
relates_to_ids_related=_agg_ids("issue_id", relation_type="relates_to", related_issue_id=issue_id),
|
||||
start_after_ids=_agg_ids("issue_id", relation_type="start_before", related_issue_id=issue_id),
|
||||
start_before_ids=_agg_ids("related_issue_id", relation_type="start_before", issue_id=issue_id),
|
||||
finish_after_ids=_agg_ids("issue_id", relation_type="finish_before", related_issue_id=issue_id),
|
||||
finish_before_ids=_agg_ids("related_issue_id", relation_type="finish_before", issue_id=issue_id),
|
||||
)
|
||||
|
||||
response_data = {
|
||||
"blocking": relation_ids["blocking_ids"],
|
||||
"blocked_by": relation_ids["blocked_by_ids"],
|
||||
"duplicate": list(set(relation_ids["duplicate_ids"] + relation_ids["duplicate_ids_related"])),
|
||||
"relates_to": list(set(relation_ids["relates_to_ids"] + relation_ids["relates_to_ids_related"])),
|
||||
"start_after": relation_ids["start_after_ids"],
|
||||
"start_before": relation_ids["start_before_ids"],
|
||||
"finish_after": relation_ids["finish_after_ids"],
|
||||
"finish_before": relation_ids["finish_before_ids"],
|
||||
}
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
@work_item_relation_docs(
|
||||
operation_id="create_work_item_relation",
|
||||
summary="Create work item relation",
|
||||
description="Create relationships between work items. Supports various relation types including blocking, blocked_by, duplicate, relates_to, start_before, start_after, finish_before, and finish_after.", # noqa E501
|
||||
parameters=[
|
||||
ISSUE_ID_PARAMETER,
|
||||
],
|
||||
request=OpenApiRequest(
|
||||
request=IssueRelationCreateSerializer,
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name="Create blocking relation",
|
||||
value={
|
||||
"relation_type": "blocking",
|
||||
"issues": [
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"550e8400-e29b-41d4-a716-446655440001",
|
||||
],
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
responses={
|
||||
201: OpenApiResponse(
|
||||
description="Work item relations created successfully",
|
||||
response=IssueRelationSerializer(many=True),
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name="Relations created",
|
||||
value=[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Fix authentication bug",
|
||||
"sequence_id": 42,
|
||||
"project_id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"relation_type": "blocked_by",
|
||||
"state_id": "550e8400-e29b-41d4-a716-446655440002",
|
||||
"priority": "high",
|
||||
"created_at": "2024-01-15T10:00:00Z",
|
||||
"updated_at": "2024-01-15T10:00:00Z",
|
||||
"created_by": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"updated_by": "550e8400-e29b-41d4-a716-446655440004",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
400: INVALID_REQUEST_RESPONSE,
|
||||
404: ISSUE_NOT_FOUND_RESPONSE,
|
||||
},
|
||||
)
|
||||
def post(self, request, slug, project_id, issue_id):
|
||||
"""Create work item relation
|
||||
|
||||
Create relationships between work items with specified relation type.
|
||||
Automatically tracks relation creation activity.
|
||||
"""
|
||||
# Validate request data using serializer
|
||||
serializer = IssueRelationCreateSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
relation_type = serializer.validated_data["relation_type"]
|
||||
issues = serializer.validated_data["issues"]
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
|
||||
actual_relation = get_actual_relation(relation_type)
|
||||
is_reverse = relation_type in ["blocking", "start_after", "finish_after"]
|
||||
|
||||
IssueRelation.objects.bulk_create(
|
||||
[
|
||||
IssueRelation(
|
||||
issue_id=(issue if is_reverse else issue_id),
|
||||
related_issue_id=(issue_id if is_reverse else issue),
|
||||
relation_type=actual_relation,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for issue in issues
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="issue_relation.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
|
||||
# Re-fetch with select_related to avoid N+1 queries in serializers.
|
||||
# bulk_create with ignore_conflicts=True may not return PKs,
|
||||
# so query by the issue/related_issue pairs and relation type.
|
||||
if is_reverse:
|
||||
refetch_filter = Q(
|
||||
issue_id__in=issues,
|
||||
related_issue_id=issue_id,
|
||||
relation_type=actual_relation,
|
||||
)
|
||||
else:
|
||||
refetch_filter = Q(
|
||||
issue_id=issue_id,
|
||||
related_issue_id__in=issues,
|
||||
relation_type=actual_relation,
|
||||
)
|
||||
|
||||
refetched_relations = IssueRelation.objects.filter(
|
||||
refetch_filter,
|
||||
workspace__slug=slug,
|
||||
).select_related(
|
||||
"issue__state",
|
||||
"related_issue__state",
|
||||
)
|
||||
|
||||
serializer_class = RelatedIssueSerializer if is_reverse else IssueRelationSerializer
|
||||
return Response(
|
||||
serializer_class(refetched_relations, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,17 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
|
||||
def _wrapped_view(instance, request, *args, **kwargs):
|
||||
# Check for creator if required
|
||||
if creator and model:
|
||||
# check if the user is part of the workspace or not
|
||||
if not WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=kwargs["slug"],
|
||||
is_active=True,
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "You don't have the required permissions."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
obj = model.objects.filter(id=kwargs["pk"], created_by=request.user).exists()
|
||||
if obj:
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from django.urls import path
|
||||
from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint
|
||||
from plane.app.views import ApiTokenEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
# API Tokens
|
||||
@@ -17,10 +17,5 @@ urlpatterns = [
|
||||
ApiTokenEndpoint.as_view(),
|
||||
name="api-tokens-details",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/service-api-tokens/",
|
||||
ServiceApiTokenEndpoint.as_view(),
|
||||
name="service-api-tokens",
|
||||
),
|
||||
## End API Tokens
|
||||
]
|
||||
|
||||
@@ -165,7 +165,7 @@ from .module.issue import ModuleIssueViewSet
|
||||
|
||||
from .module.archive import ModuleArchiveUnarchiveEndpoint
|
||||
|
||||
from .api import ApiTokenEndpoint, ServiceApiTokenEndpoint
|
||||
from .api import ApiTokenEndpoint
|
||||
|
||||
from .page.base import (
|
||||
PageViewSet,
|
||||
|
||||
@@ -13,9 +13,8 @@ from rest_framework import status
|
||||
|
||||
# Module import
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import APIToken, Workspace
|
||||
from plane.db.models import APIToken
|
||||
from plane.app.serializers import APITokenSerializer, APITokenReadSerializer
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
|
||||
|
||||
class ApiTokenEndpoint(BaseAPIView):
|
||||
@@ -61,28 +60,3 @@ class ApiTokenEndpoint(BaseAPIView):
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ServiceApiTokenEndpoint(BaseAPIView):
|
||||
permission_classes = [WorkspaceEntityPermission]
|
||||
|
||||
def post(self, request: Request, slug: str) -> Response:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
api_token = APIToken.objects.filter(workspace=workspace, is_service=True).first()
|
||||
|
||||
if api_token:
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
# Check the user type
|
||||
user_type = 1 if request.user.is_bot else 0
|
||||
|
||||
api_token = APIToken.objects.create(
|
||||
label=str(uuid4().hex),
|
||||
description="Service Token",
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
user_type=user_type,
|
||||
is_service=True,
|
||||
)
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -64,7 +64,10 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, issue_id=issue_id
|
||||
).first()
|
||||
if not issue_attachment:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(
|
||||
{"error": "Issue attachment not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
issue_attachment.asset.delete(save=False)
|
||||
issue_attachment.delete()
|
||||
issue_activity.delay(
|
||||
|
||||
@@ -1118,7 +1118,7 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
|
||||
epoch = int(timezone.now().timestamp())
|
||||
|
||||
# Fetch all relevant issues in a single query
|
||||
issues = list(Issue.objects.filter(id__in=issue_ids))
|
||||
issues = list(Issue.objects.filter(id__in=issue_ids, workspace__slug=slug, project_id=project_id))
|
||||
issues_dict = {str(issue.id): issue for issue in issues}
|
||||
issues_to_update = []
|
||||
|
||||
|
||||
@@ -226,21 +226,36 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
if workspace_role in [5] and int(request.data.get("role", project_member.role)) in [15, 20]:
|
||||
return Response(
|
||||
{"error": "You cannot add a user with role higher than the workspace role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if "role" in request.data:
|
||||
# Only Admins can modify roles
|
||||
if requested_project_member.role < ROLE.ADMIN.value and not is_workspace_admin:
|
||||
return Response(
|
||||
{"error": "You do not have permission to update roles"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
if (
|
||||
"role" in request.data
|
||||
and int(request.data.get("role", project_member.role)) > requested_project_member.role
|
||||
and not is_workspace_admin
|
||||
):
|
||||
return Response(
|
||||
{"error": "You cannot update a role that is higher than your own role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# Cannot modify a member whose role is equal to or higher than your own
|
||||
if project_member.role >= requested_project_member.role and not is_workspace_admin:
|
||||
return Response(
|
||||
{"error": "You cannot update the role of a member with a role equal to or higher than your own"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
new_role = int(request.data.get("role"))
|
||||
|
||||
# Cannot assign a role equal to or higher than your own
|
||||
if new_role >= requested_project_member.role and not is_workspace_admin:
|
||||
return Response(
|
||||
{"error": "You cannot assign a role equal to or higher than your own"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Cannot assign a role higher than the target's workspace role
|
||||
if workspace_role in [5] and new_role in [15, 20]:
|
||||
return Response(
|
||||
{"error": "You cannot add a user with role higher than the workspace role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse, urljoin
|
||||
import base64
|
||||
import ipaddress
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Tuple
|
||||
from typing import Optional
|
||||
from plane.db.models import IssueLink
|
||||
from plane.utils.exception_logger import log_exception
|
||||
@@ -66,6 +66,52 @@ def validate_url_ip(url: str) -> None:
|
||||
MAX_REDIRECTS = 5
|
||||
|
||||
|
||||
def safe_get(
|
||||
url: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: int = 1,
|
||||
) -> Tuple[requests.Response, str]:
|
||||
"""
|
||||
Perform a GET request that validates every redirect hop against private IPs.
|
||||
Prevents SSRF by ensuring no redirect lands on a private/internal address.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch
|
||||
headers: Optional request headers
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
A tuple of (final Response object, final URL after redirects)
|
||||
|
||||
Raises:
|
||||
ValueError: If any URL in the redirect chain points to a private IP
|
||||
requests.RequestException: On network errors
|
||||
RuntimeError: If max redirects exceeded
|
||||
"""
|
||||
validate_url_ip(url)
|
||||
|
||||
current_url = url
|
||||
response = requests.get(
|
||||
current_url, headers=headers, timeout=timeout, allow_redirects=False
|
||||
)
|
||||
|
||||
redirect_count = 0
|
||||
while response.is_redirect:
|
||||
if redirect_count >= MAX_REDIRECTS:
|
||||
raise RuntimeError(f"Too many redirects for URL: {url}")
|
||||
redirect_url = response.headers.get("Location")
|
||||
if not redirect_url:
|
||||
break
|
||||
current_url = urljoin(current_url, redirect_url)
|
||||
validate_url_ip(current_url)
|
||||
redirect_count += 1
|
||||
response = requests.get(
|
||||
current_url, headers=headers, timeout=timeout, allow_redirects=False
|
||||
)
|
||||
|
||||
return response, current_url
|
||||
|
||||
|
||||
def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Crawls a URL to extract the title and favicon.
|
||||
@@ -86,26 +132,8 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
title = None
|
||||
final_url = url
|
||||
|
||||
validate_url_ip(final_url)
|
||||
|
||||
try:
|
||||
# Manually follow redirects to validate each URL before requesting
|
||||
redirect_count = 0
|
||||
response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
|
||||
|
||||
while response.is_redirect and redirect_count < MAX_REDIRECTS:
|
||||
redirect_url = response.headers.get("Location")
|
||||
if not redirect_url:
|
||||
break
|
||||
# Resolve relative redirects against current URL
|
||||
final_url = urljoin(final_url, redirect_url)
|
||||
# Validate the redirect target BEFORE making the request
|
||||
validate_url_ip(final_url)
|
||||
redirect_count += 1
|
||||
response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
|
||||
|
||||
if redirect_count >= MAX_REDIRECTS:
|
||||
logger.warning(f"Too many redirects for URL: {url}")
|
||||
response, final_url = safe_get(url, headers=headers)
|
||||
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
title_tag = soup.find("title")
|
||||
@@ -113,8 +141,10 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.warning(f"Failed to fetch HTML for title: {str(e)}")
|
||||
except (ValueError, RuntimeError) as e:
|
||||
logger.warning(f"URL validation failed: {str(e)}")
|
||||
|
||||
# Fetch and encode favicon using final URL (after redirects)
|
||||
# Fetch and encode favicon using final URL (after redirects) for correct relative href resolution
|
||||
favicon_base64 = fetch_and_encode_favicon(headers, soup, final_url)
|
||||
|
||||
# Prepare result
|
||||
@@ -204,9 +234,7 @@ def fetch_and_encode_favicon(
|
||||
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
|
||||
}
|
||||
|
||||
validate_url_ip(favicon_url)
|
||||
|
||||
response = requests.get(favicon_url, headers=headers, timeout=1)
|
||||
response, _ = safe_get(favicon_url, headers=headers)
|
||||
|
||||
# Get content type
|
||||
content_type = response.headers.get("content-type", "image/x-icon")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.28 on 2026-02-26 14:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0120_issueview_archived_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='estimate',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('categories', 'Categories'), ('points', 'Points')], default='categories', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -10,11 +10,15 @@ from django.db.models import Q
|
||||
# Module imports
|
||||
from .project import ProjectBaseModel
|
||||
|
||||
class EstimateType(models.TextChoices):
|
||||
CATEGORIES = "categories", "Categories"
|
||||
POINTS = "points", "Points"
|
||||
|
||||
|
||||
class Estimate(ProjectBaseModel):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(verbose_name="Estimate Description", blank=True)
|
||||
type = models.CharField(max_length=255, default="categories")
|
||||
type = models.CharField(max_length=255, choices=EstimateType.choices, default=EstimateType.CATEGORIES)
|
||||
last_used = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from plane.bgtasks.work_item_link_task import safe_get, validate_url_ip
|
||||
|
||||
|
||||
def _make_response(status_code=200, headers=None, is_redirect=False, content=b""):
|
||||
"""Create a mock requests.Response."""
|
||||
resp = MagicMock()
|
||||
resp.status_code = status_code
|
||||
resp.is_redirect = is_redirect
|
||||
resp.headers = headers or {}
|
||||
resp.content = content
|
||||
return resp
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestValidateUrlIp:
|
||||
"""Test validate_url_ip blocks private/internal IPs."""
|
||||
|
||||
def test_rejects_private_ip(self):
|
||||
with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
|
||||
mock_dns.return_value = [(None, None, None, None, ("192.168.1.1", 0))]
|
||||
with pytest.raises(ValueError, match="private/internal"):
|
||||
validate_url_ip("http://example.com")
|
||||
|
||||
def test_rejects_loopback(self):
|
||||
with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
|
||||
mock_dns.return_value = [(None, None, None, None, ("127.0.0.1", 0))]
|
||||
with pytest.raises(ValueError, match="private/internal"):
|
||||
validate_url_ip("http://example.com")
|
||||
|
||||
def test_rejects_non_http_scheme(self):
|
||||
with pytest.raises(ValueError, match="Only HTTP and HTTPS"):
|
||||
validate_url_ip("file:///etc/passwd")
|
||||
|
||||
def test_allows_public_ip(self):
|
||||
with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
|
||||
mock_dns.return_value = [(None, None, None, None, ("93.184.216.34", 0))]
|
||||
validate_url_ip("https://example.com") # Should not raise
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSafeGet:
|
||||
"""Test safe_get follows redirects safely and blocks SSRF."""
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_returns_response_for_non_redirect(self, mock_validate, mock_get):
|
||||
final_resp = _make_response(status_code=200, content=b"OK")
|
||||
mock_get.return_value = final_resp
|
||||
|
||||
response, final_url = safe_get("https://example.com")
|
||||
|
||||
assert response is final_resp
|
||||
assert final_url == "https://example.com"
|
||||
mock_validate.assert_called_once_with("https://example.com")
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_follows_redirect_and_validates_each_hop(self, mock_validate, mock_get):
|
||||
redirect_resp = _make_response(
|
||||
status_code=301,
|
||||
is_redirect=True,
|
||||
headers={"Location": "https://other.com/page"},
|
||||
)
|
||||
final_resp = _make_response(status_code=200, content=b"OK")
|
||||
mock_get.side_effect = [redirect_resp, final_resp]
|
||||
|
||||
response, final_url = safe_get("https://example.com")
|
||||
|
||||
assert response is final_resp
|
||||
assert final_url == "https://other.com/page"
|
||||
# Should validate both the initial URL and the redirect target
|
||||
assert mock_validate.call_count == 2
|
||||
mock_validate.assert_any_call("https://example.com")
|
||||
mock_validate.assert_any_call("https://other.com/page")
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_blocks_redirect_to_private_ip(self, mock_validate, mock_get):
|
||||
redirect_resp = _make_response(
|
||||
status_code=302,
|
||||
is_redirect=True,
|
||||
headers={"Location": "http://192.168.1.1:8080"},
|
||||
)
|
||||
mock_get.return_value = redirect_resp
|
||||
# First call (initial URL) succeeds, second call (redirect target) fails
|
||||
mock_validate.side_effect = [None, ValueError("Access to private/internal networks is not allowed")]
|
||||
|
||||
with pytest.raises(ValueError, match="private/internal"):
|
||||
safe_get("https://evil.com/redirect")
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_raises_on_too_many_redirects(self, mock_validate, mock_get):
|
||||
redirect_resp = _make_response(
|
||||
status_code=302,
|
||||
is_redirect=True,
|
||||
headers={"Location": "https://example.com/loop"},
|
||||
)
|
||||
mock_get.return_value = redirect_resp
|
||||
|
||||
with pytest.raises(RuntimeError, match="Too many redirects"):
|
||||
safe_get("https://example.com/start")
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_succeeds_at_exact_max_redirects(self, mock_validate, mock_get):
|
||||
"""After exactly MAX_REDIRECTS hops, if the final response is 200, it should succeed."""
|
||||
redirect_resp = _make_response(
|
||||
status_code=302,
|
||||
is_redirect=True,
|
||||
headers={"Location": "https://example.com/next"},
|
||||
)
|
||||
final_resp = _make_response(status_code=200, content=b"OK")
|
||||
# 5 redirects then a 200
|
||||
mock_get.side_effect = [redirect_resp] * 5 + [final_resp]
|
||||
|
||||
response, final_url = safe_get("https://example.com/start")
|
||||
|
||||
assert response is final_resp
|
||||
assert not response.is_redirect
|
||||
@@ -47,6 +47,7 @@ from .parameters import (
|
||||
CYCLE_VIEW_PARAMETER,
|
||||
FIELDS_PARAMETER,
|
||||
EXPAND_PARAMETER,
|
||||
ESTIMATE_ID_PARAMETER,
|
||||
)
|
||||
|
||||
# Responses
|
||||
@@ -126,6 +127,10 @@ from .examples import (
|
||||
STATE_UPDATE_EXAMPLE,
|
||||
INTAKE_ISSUE_CREATE_EXAMPLE,
|
||||
INTAKE_ISSUE_UPDATE_EXAMPLE,
|
||||
ESTIMATE_CREATE_EXAMPLE,
|
||||
ESTIMATE_UPDATE_EXAMPLE,
|
||||
ESTIMATE_POINT_CREATE_EXAMPLE,
|
||||
ESTIMATE_POINT_UPDATE_EXAMPLE,
|
||||
# Response Examples
|
||||
CYCLE_EXAMPLE,
|
||||
TRANSFER_CYCLE_ISSUE_SUCCESS_EXAMPLE,
|
||||
@@ -145,6 +150,8 @@ from .examples import (
|
||||
PROJECT_MEMBER_EXAMPLE,
|
||||
CYCLE_ISSUE_EXAMPLE,
|
||||
STICKY_EXAMPLE,
|
||||
ESTIMATE_EXAMPLE,
|
||||
ESTIMATE_POINT_EXAMPLE,
|
||||
)
|
||||
|
||||
# Helper decorators
|
||||
@@ -157,6 +164,7 @@ from .decorators import (
|
||||
user_docs,
|
||||
cycle_docs,
|
||||
work_item_docs,
|
||||
work_item_relation_docs,
|
||||
label_docs,
|
||||
issue_link_docs,
|
||||
issue_comment_docs,
|
||||
@@ -165,6 +173,8 @@ from .decorators import (
|
||||
module_docs,
|
||||
module_issue_docs,
|
||||
state_docs,
|
||||
estimate_docs,
|
||||
estimate_point_docs,
|
||||
)
|
||||
|
||||
# Schema processing hooks
|
||||
@@ -206,6 +216,7 @@ __all__ = [
|
||||
"CYCLE_VIEW_PARAMETER",
|
||||
"FIELDS_PARAMETER",
|
||||
"EXPAND_PARAMETER",
|
||||
"ESTIMATE_ID_PARAMETER",
|
||||
# Responses
|
||||
"UNAUTHORIZED_RESPONSE",
|
||||
"FORBIDDEN_RESPONSE",
|
||||
@@ -279,6 +290,10 @@ __all__ = [
|
||||
"STATE_UPDATE_EXAMPLE",
|
||||
"INTAKE_ISSUE_CREATE_EXAMPLE",
|
||||
"INTAKE_ISSUE_UPDATE_EXAMPLE",
|
||||
"ESTIMATE_CREATE_EXAMPLE",
|
||||
"ESTIMATE_UPDATE_EXAMPLE",
|
||||
"ESTIMATE_POINT_CREATE_EXAMPLE",
|
||||
"ESTIMATE_POINT_UPDATE_EXAMPLE",
|
||||
# Response Examples
|
||||
"CYCLE_EXAMPLE",
|
||||
"TRANSFER_CYCLE_ISSUE_SUCCESS_EXAMPLE",
|
||||
@@ -298,6 +313,8 @@ __all__ = [
|
||||
"PROJECT_MEMBER_EXAMPLE",
|
||||
"CYCLE_ISSUE_EXAMPLE",
|
||||
"STICKY_EXAMPLE",
|
||||
"ESTIMATE_EXAMPLE",
|
||||
"ESTIMATE_POINT_EXAMPLE",
|
||||
# Decorators
|
||||
"workspace_docs",
|
||||
"project_docs",
|
||||
@@ -307,6 +324,7 @@ __all__ = [
|
||||
"user_docs",
|
||||
"cycle_docs",
|
||||
"work_item_docs",
|
||||
"work_item_relation_docs",
|
||||
"label_docs",
|
||||
"issue_link_docs",
|
||||
"issue_comment_docs",
|
||||
@@ -315,6 +333,8 @@ __all__ = [
|
||||
"module_docs",
|
||||
"module_issue_docs",
|
||||
"state_docs",
|
||||
"estimate_docs",
|
||||
"estimate_point_docs",
|
||||
# Hooks
|
||||
"preprocess_filter_api_v1_paths",
|
||||
"generate_operation_summary",
|
||||
|
||||
@@ -223,6 +223,21 @@ def issue_attachment_docs(**kwargs):
|
||||
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||
|
||||
|
||||
def work_item_relation_docs(**kwargs):
|
||||
"""Decorator for work item relation endpoints"""
|
||||
defaults = {
|
||||
"tags": ["Work Item Relations"],
|
||||
"parameters": [WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||
"responses": {
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
403: FORBIDDEN_RESPONSE,
|
||||
404: NOT_FOUND_RESPONSE,
|
||||
},
|
||||
}
|
||||
|
||||
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||
|
||||
|
||||
def module_docs(**kwargs):
|
||||
"""Decorator for module management endpoints"""
|
||||
defaults = {
|
||||
@@ -282,3 +297,29 @@ def sticky_docs(**kwargs):
|
||||
}
|
||||
|
||||
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||
|
||||
def estimate_docs(**kwargs):
|
||||
"""Decorator for estimate-related endpoints"""
|
||||
defaults = {
|
||||
"tags": ["Estimates"],
|
||||
"parameters": [WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||
"responses": {
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
403: FORBIDDEN_RESPONSE,
|
||||
404: NOT_FOUND_RESPONSE,
|
||||
},
|
||||
}
|
||||
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||
|
||||
def estimate_point_docs(**kwargs):
|
||||
"""Decorator for estimate point-related endpoints"""
|
||||
defaults = {
|
||||
"tags": ["Estimate Points"],
|
||||
"parameters": [WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||
"responses": {
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
403: FORBIDDEN_RESPONSE,
|
||||
404: NOT_FOUND_RESPONSE,
|
||||
},
|
||||
}
|
||||
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||
@@ -686,6 +686,69 @@ STICKY_EXAMPLE = OpenApiExample(
|
||||
},
|
||||
)
|
||||
|
||||
# Estimate Examples
|
||||
ESTIMATE_EXAMPLE = OpenApiExample(
|
||||
name="Estimate",
|
||||
value={
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Estimate 1",
|
||||
"description": "Estimate 1 description",
|
||||
},
|
||||
description="Example response for an estimate",
|
||||
)
|
||||
|
||||
ESTIMATE_POINT_EXAMPLE = OpenApiExample(
|
||||
name="EstimatePoint",
|
||||
value={
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"estimate": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"key": 1,
|
||||
"value": "1",
|
||||
},
|
||||
description="Example response for an estimate point",
|
||||
)
|
||||
ESTIMATE_CREATE_EXAMPLE = OpenApiExample(
|
||||
name="EstimateCreateSerializer",
|
||||
value={
|
||||
"name": "Estimate 1",
|
||||
"description": "Estimate 1 description",
|
||||
},
|
||||
description="Example request for creating an estimate",
|
||||
)
|
||||
ESTIMATE_UPDATE_EXAMPLE = OpenApiExample(
|
||||
name="EstimateUpdateSerializer",
|
||||
value={
|
||||
"name": "Estimate 1",
|
||||
"description": "Estimate 1 description",
|
||||
},
|
||||
description="Example request for updating an estimate",
|
||||
)
|
||||
|
||||
# Estimate Point Examples
|
||||
ESTIMATE_POINT_CREATE_EXAMPLE = OpenApiExample(
|
||||
name="EstimatePointCreateSerializer",
|
||||
value=[
|
||||
{
|
||||
"value": "1",
|
||||
"description": "Estimate Point 1 description",
|
||||
},
|
||||
{
|
||||
"value": "2",
|
||||
"description": "Estimate Point 2 description",
|
||||
},
|
||||
],
|
||||
description="Example request for creating an estimate point",
|
||||
)
|
||||
ESTIMATE_POINT_UPDATE_EXAMPLE = OpenApiExample(
|
||||
name="EstimatePointUpdateSerializer",
|
||||
value={
|
||||
"value": "1",
|
||||
"description": "Estimate Point 1 description",
|
||||
},
|
||||
description="Example request for updating an estimate point",
|
||||
)
|
||||
|
||||
|
||||
# Sample data for different entity types
|
||||
SAMPLE_ISSUE = {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
@@ -801,6 +864,24 @@ SAMPLE_STICKY = {
|
||||
"created_at": "2024-01-01T10:30:00Z",
|
||||
}
|
||||
|
||||
SAMPLE_ESTIMATE = {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Estimate 1",
|
||||
"description": "Estimate 1 description",
|
||||
"type": "categories",
|
||||
"last_used": False,
|
||||
"created_at": "2024-01-01T10:30:00Z",
|
||||
}
|
||||
|
||||
SAMPLE_ESTIMATE_POINT = {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"estimate": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"key": 1,
|
||||
"value": "1",
|
||||
"description": "Estimate Point 1 description",
|
||||
"created_at": "2024-01-01T10:30:00Z",
|
||||
}
|
||||
|
||||
# Mapping of schema types to sample data
|
||||
SCHEMA_EXAMPLES = {
|
||||
"Issue": SAMPLE_ISSUE,
|
||||
@@ -816,6 +897,8 @@ SCHEMA_EXAMPLES = {
|
||||
"Intake": SAMPLE_INTAKE,
|
||||
"CycleIssue": SAMPLE_CYCLE_ISSUE,
|
||||
"Sticky": SAMPLE_STICKY,
|
||||
"Estimate": SAMPLE_ESTIMATE,
|
||||
"EstimatePoint": SAMPLE_ESTIMATE_POINT,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -495,3 +495,11 @@ EXPAND_PARAMETER = OpenApiParameter(
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
ESTIMATE_ID_PARAMETER = OpenApiParameter(
|
||||
name="estimate_id",
|
||||
description="Estimate ID",
|
||||
required=True,
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.PATH,
|
||||
)
|
||||
|
||||
@@ -45,13 +45,13 @@ scout-apm==3.1.0
|
||||
# xlsx generation
|
||||
openpyxl==3.1.2
|
||||
# logging
|
||||
python-json-logger==3.3.0
|
||||
python-json-logger==4.0.0
|
||||
# html parser
|
||||
beautifulsoup4==4.12.3
|
||||
# analytics
|
||||
posthog==3.5.0
|
||||
# crypto
|
||||
cryptography==46.0.5
|
||||
cryptography==46.0.6
|
||||
# html validator
|
||||
lxml==6.0.0
|
||||
# s3
|
||||
@@ -61,7 +61,7 @@ zxcvbn==4.4.28
|
||||
# timezone
|
||||
pytz==2024.1
|
||||
# jwt
|
||||
PyJWT==2.8.0
|
||||
PyJWT==2.12.0
|
||||
# OpenTelemetry
|
||||
opentelemetry-api==1.28.1
|
||||
opentelemetry-sdk==1.28.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-r base.txt
|
||||
# test framework
|
||||
pytest==7.4.0
|
||||
pytest==9.0.2
|
||||
pytest-django==4.5.2
|
||||
pytest-cov==4.1.0
|
||||
pytest-xdist==3.3.1
|
||||
@@ -9,4 +9,4 @@ factory-boy==3.3.0
|
||||
freezegun==1.2.2
|
||||
coverage==7.2.7
|
||||
httpx==0.24.1
|
||||
requests==2.32.4
|
||||
requests==2.33.0
|
||||
@@ -15,7 +15,7 @@ RUN apk update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ARG TURBO_VERSION=2.8.12
|
||||
ARG TURBO_VERSION=2.9.4
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
RUN turbo prune --scope=live --docker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"description": "A realtime collaborative server powers Plane's rich text editor",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && tsdown",
|
||||
"dev": "tsdown --watch --onSuccess \"node --env-file=.env .\"",
|
||||
"dev": "tsdown --watch --no-clean --onSuccess \"node --env-file=.env .\"",
|
||||
"start": "node --env-file=.env .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
@@ -27,7 +27,6 @@
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dotenvx/dotenvx": "catalog:",
|
||||
"@effect/platform": "^0.94.0",
|
||||
"@effect/platform-node": "^0.104.0",
|
||||
"@fontsource/inter": "5.2.8",
|
||||
@@ -47,7 +46,8 @@
|
||||
"axios": "catalog:",
|
||||
"compression": "1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"effect": "^3.16.3",
|
||||
"dotenv": "catalog:",
|
||||
"effect": "3.20.0",
|
||||
"express": "catalog:",
|
||||
"express-ws": "^5.0.2",
|
||||
"helmet": "^7.1.0",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import * as dotenv from "@dotenvx/dotenvx";
|
||||
import * as dotenv from "dotenv";
|
||||
import { z } from "zod";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
CODE_COLORS,
|
||||
LINK_COLORS,
|
||||
MENTION_COLORS,
|
||||
NEUTRAL_COLORS,
|
||||
TEXT_COLORS,
|
||||
} from "./colors";
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN corepack enable pnpm
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
RUN pnpm add -g turbo@2.8.12
|
||||
RUN pnpm add -g turbo@2.9.4
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -95,6 +95,6 @@ export function HydrateFallback() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
export function ErrorBoundary({ error: _error }: Route.ErrorBoundaryProps) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export type TEditorFlaggingHookReturnType = {
|
||||
/**
|
||||
* @description extensions disabled in various editors
|
||||
*/
|
||||
export const useEditorFlagging = (anchor: string): TEditorFlaggingHookReturnType => ({
|
||||
export const useEditorFlagging = (_anchor: string): TEditorFlaggingHookReturnType => ({
|
||||
document: {
|
||||
disabled: [],
|
||||
flagged: [],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
@@ -53,7 +53,6 @@
|
||||
"uuid": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "catalog:",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@react-router/dev": "catalog:",
|
||||
@@ -62,6 +61,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { SitesCycleService } from "@plane/services";
|
||||
import type { TPublicCycle } from "@/types/cycle";
|
||||
// store
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
import type { RootStore } from "./root.store";
|
||||
|
||||
export interface ICycleStore {
|
||||
// observables
|
||||
@@ -23,9 +23,9 @@ export interface ICycleStore {
|
||||
export class CycleStore implements ICycleStore {
|
||||
cycles: TPublicCycle[] | undefined = undefined;
|
||||
cycleService: SitesCycleService;
|
||||
rootStore: CoreRootStore;
|
||||
rootStore: RootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
cycles: observable,
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
} from "@plane/types";
|
||||
// types
|
||||
import type { IIssue, TIssuesResponse } from "@/types/issue";
|
||||
import type { CoreRootStore } from "../root.store";
|
||||
import type { RootStore } from "../root.store";
|
||||
// constants
|
||||
// helpers
|
||||
|
||||
@@ -81,7 +81,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
// root store
|
||||
rootIssueStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
loader: observable,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { observable, action, makeObservable, runInAction } from "mobx";
|
||||
import { InstanceService } from "@plane/services";
|
||||
import type { IInstance, IInstanceConfig } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
|
||||
type TError = {
|
||||
status: string;
|
||||
@@ -40,7 +40,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
// services
|
||||
instanceService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
isLoading: observable.ref,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { SitesFileService, SitesIssueService } from "@plane/services";
|
||||
import type { TFileSignedURLResponse, TIssuePublicComment } from "@plane/types";
|
||||
import { EFileAssetType } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
// types
|
||||
import type { IIssue, IPeekMode, IVote } from "@/types/issue";
|
||||
|
||||
@@ -60,12 +60,12 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
[key: string]: IIssue;
|
||||
} = {};
|
||||
// root store
|
||||
rootStore: CoreRootStore;
|
||||
rootStore: RootStore;
|
||||
// services
|
||||
issueService: SitesIssueService;
|
||||
fileService: SitesFileService;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
loader: observable.ref,
|
||||
error: observable.ref,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { computedFn } from "mobx-utils";
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@plane/constants";
|
||||
import type { IssuePaginationOptions, TIssueParams } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
// types
|
||||
import type {
|
||||
TIssueLayoutOptions,
|
||||
@@ -60,7 +60,7 @@ export class IssueFilterStore implements IIssueFilterStore {
|
||||
};
|
||||
filters: { [anchor: string]: TIssueFilters } | undefined = undefined;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
layoutOptions: observable,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { action, makeObservable, runInAction } from "mobx";
|
||||
import { SitesIssueService } from "@plane/services";
|
||||
import type { IssuePaginationOptions, TLoader } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
// types
|
||||
import { BaseIssuesStore } from "./helpers/base-issues.store";
|
||||
import type { IBaseIssuesStore } from "./helpers/base-issues.store";
|
||||
@@ -28,11 +28,11 @@ export interface IIssueStore extends IBaseIssuesStore {
|
||||
|
||||
export class IssueStore extends BaseIssuesStore implements IIssueStore {
|
||||
// root store
|
||||
rootStore: CoreRootStore;
|
||||
rootStore: RootStore;
|
||||
// services
|
||||
issueService: SitesIssueService;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
constructor(_rootStore: RootStore) {
|
||||
super(_rootStore);
|
||||
makeObservable(this, {
|
||||
// actions
|
||||
|
||||
@@ -10,7 +10,7 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
|
||||
import { SitesLabelService } from "@plane/services";
|
||||
import type { IIssueLabel } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
import type { RootStore } from "./root.store";
|
||||
|
||||
export interface IIssueLabelStore {
|
||||
// observables
|
||||
@@ -25,9 +25,9 @@ export interface IIssueLabelStore {
|
||||
export class LabelStore implements IIssueLabelStore {
|
||||
labelMap: Record<string, IIssueLabel> = {};
|
||||
labelService: SitesLabelService;
|
||||
rootStore: CoreRootStore;
|
||||
rootStore: RootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
labelMap: observable,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
|
||||
// plane imports
|
||||
import { SitesMemberService } from "@plane/services";
|
||||
import type { TPublicMember } from "@/types/member";
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
import type { RootStore } from "./root.store";
|
||||
|
||||
export interface IIssueMemberStore {
|
||||
// observables
|
||||
@@ -24,9 +24,9 @@ export interface IIssueMemberStore {
|
||||
export class MemberStore implements IIssueMemberStore {
|
||||
memberMap: Record<string, TPublicMember> = {};
|
||||
memberService: SitesMemberService;
|
||||
rootStore: CoreRootStore;
|
||||
rootStore: RootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
memberMap: observable,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SitesModuleService } from "@plane/services";
|
||||
// types
|
||||
import type { TPublicModule } from "@/types/modules";
|
||||
// root store
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
import type { RootStore } from "./root.store";
|
||||
|
||||
export interface IIssueModuleStore {
|
||||
// observables
|
||||
@@ -26,9 +26,9 @@ export interface IIssueModuleStore {
|
||||
export class ModuleStore implements IIssueModuleStore {
|
||||
moduleMap: Record<string, TPublicModule> = {};
|
||||
moduleService: SitesModuleService;
|
||||
rootStore: CoreRootStore;
|
||||
rootStore: RootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
moduleMap: observable,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { UserService } from "@plane/services";
|
||||
import type { TUserProfile } from "@plane/types";
|
||||
import { EStartOfTheWeek } from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
|
||||
type TError = {
|
||||
status: string;
|
||||
@@ -64,7 +64,7 @@ export class ProfileStore implements IProfileStore {
|
||||
// services
|
||||
userService: UserService;
|
||||
|
||||
constructor(public store: CoreRootStore) {
|
||||
constructor(public store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isLoading: observable.ref,
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
TProjectPublishViewProps,
|
||||
} from "@plane/types";
|
||||
// store
|
||||
import type { CoreRootStore } from "../root.store";
|
||||
import type { RootStore } from "../root.store";
|
||||
|
||||
export interface IPublishStore extends TProjectPublishSettings {
|
||||
// computed
|
||||
@@ -45,7 +45,7 @@ export class PublishStore implements IPublishStore {
|
||||
workspace_detail: IWorkspaceLite | undefined;
|
||||
|
||||
constructor(
|
||||
private store: CoreRootStore,
|
||||
private store: RootStore,
|
||||
publishSettings: TProjectPublishSettings
|
||||
) {
|
||||
this.anchor = publishSettings.anchor;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SitesProjectPublishService } from "@plane/services";
|
||||
import type { TProjectPublishSettings } from "@plane/types";
|
||||
// store
|
||||
import { PublishStore } from "@/store/publish/publish.store";
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
|
||||
export interface IPublishListStore {
|
||||
// observables
|
||||
@@ -26,7 +26,7 @@ export class PublishListStore implements IPublishListStore {
|
||||
// service
|
||||
publishService;
|
||||
|
||||
constructor(private rootStore: CoreRootStore) {
|
||||
constructor(private rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
publishMap: observable,
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { IState } from "@plane/types";
|
||||
// helpers
|
||||
import { sortStates } from "@/helpers/state.helper";
|
||||
// store
|
||||
import type { CoreRootStore } from "./root.store";
|
||||
import type { RootStore } from "./root.store";
|
||||
|
||||
export interface IStateStore {
|
||||
// observables
|
||||
@@ -28,9 +28,9 @@ export interface IStateStore {
|
||||
export class StateStore implements IStateStore {
|
||||
states: IState[] | undefined = undefined;
|
||||
stateService: SitesStateService;
|
||||
rootStore: CoreRootStore;
|
||||
rootStore: RootStore;
|
||||
|
||||
constructor(_rootStore: CoreRootStore) {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
states: observable,
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { ActorDetail, IUser } from "@plane/types";
|
||||
import type { IProfileStore } from "@/store/profile.store";
|
||||
import { ProfileStore } from "@/store/profile.store";
|
||||
// store
|
||||
import type { CoreRootStore } from "@/store/root.store";
|
||||
import type { RootStore } from "@/store/root.store";
|
||||
|
||||
type TUserErrorStatus = {
|
||||
status: string;
|
||||
@@ -50,7 +50,7 @@ export class UserStore implements IUserStore {
|
||||
// service
|
||||
userService: UserService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
// stores
|
||||
this.profile = new ProfileStore(store);
|
||||
// service
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "node:path";
|
||||
import * as dotenv from "@dotenvx/dotenvx";
|
||||
import * as dotenv from "dotenv";
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
@@ -14,7 +14,7 @@ RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
ARG TURBO_VERSION=2.8.12
|
||||
ARG TURBO_VERSION=2.9.4
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
// ui
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { PanelRight } from "lucide-react";
|
||||
@@ -14,7 +13,6 @@ import { useTranslation } from "@plane/i18n";
|
||||
import { YourWorkIcon, ChevronDownIcon } from "@plane/propel/icons";
|
||||
import type { IUserProfileProjectSegregation } from "@plane/types";
|
||||
import { Breadcrumbs, Header, CustomMenu } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { ProfileIssuesFilter } from "@/components/profile/profile-issues-filter";
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
|
||||
@@ -81,7 +81,6 @@ function UserInvitationsPage() {
|
||||
.then(() => {
|
||||
mutate(USER_WORKSPACES_LIST);
|
||||
const firstInviteId = invitationsRespond[0];
|
||||
const invitation = invitations?.find((i) => i.id === firstInviteId);
|
||||
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
|
||||
updateUserProfile({ last_workspace_id: redirectWorkspace?.id })
|
||||
.then(() => {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -13,8 +13,6 @@ import { TranslationProvider } from "@plane/i18n";
|
||||
import { Toast } from "@plane/propel/toast";
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// polyfills
|
||||
import "@/lib/polyfills";
|
||||
// mobx store provider
|
||||
import { StoreProvider } from "@/lib/store-context";
|
||||
|
||||
@@ -31,10 +29,6 @@ const InstanceWrapper = lazy(function InstanceWrapper() {
|
||||
return import("@/lib/wrappers/instance-wrapper");
|
||||
});
|
||||
|
||||
const ChatSupportModal = lazy(function ChatSupportModal() {
|
||||
return import("@/components/global/chat-support-modal");
|
||||
});
|
||||
|
||||
export interface IAppProvider {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -53,7 +47,6 @@ export function AppProvider(props: IAppProvider) {
|
||||
<StoreWrapper>
|
||||
<InstanceWrapper>
|
||||
<Suspense>
|
||||
<ChatSupportModal />
|
||||
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
|
||||
</Suspense>
|
||||
</InstanceWrapper>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useMemo } from "react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { getAnalyticsTabs } from "./tabs";
|
||||
|
||||
export const useAnalyticsTabs = (workspaceSlug: string) => {
|
||||
export const useAnalyticsTabs = (_workspaceSlug: string) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const analyticsTabs = useMemo(() => getAnalyticsTabs(t), [t]);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
|
||||
export type TCustomAutomationsRootProps = {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
|
||||
@@ -10,6 +10,6 @@ type TProps = {
|
||||
workspace?: IWorkspace;
|
||||
};
|
||||
|
||||
export function SubscriptionPill(props: TProps) {
|
||||
export function SubscriptionPill(_props: TProps) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ type ActiveCyclesComponentProps = {
|
||||
const ActiveCyclesComponent = observer(function ActiveCyclesComponent({
|
||||
cycleId,
|
||||
activeCycle,
|
||||
activeCycleResolvedPath,
|
||||
activeCycleResolvedPath: _activeCycleResolvedPath,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
handleFiltersUpdate,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
type Props = {
|
||||
cycleId: string;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
// components
|
||||
import { SidebarChart } from "./base";
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
// local components
|
||||
|
||||
@@ -15,7 +14,6 @@ type TDeDupeButtonRoot = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export function DeDupeButtonRoot(props: TDeDupeButtonRoot) {
|
||||
const { workspaceSlug, isDuplicateModalOpen, label, handleOnClick } = props;
|
||||
export function DeDupeButtonRoot(_props: TDeDupeButtonRoot) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
// types
|
||||
import type { TDeDupeIssue } from "@plane/types";
|
||||
|
||||
@@ -14,7 +13,6 @@ type TDuplicateModalRootProps = {
|
||||
handleDuplicateIssueModal: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export function DuplicateModalRoot(props: TDuplicateModalRootProps) {
|
||||
const { workspaceSlug, issues, handleDuplicateIssueModal } = props;
|
||||
export function DuplicateModalRoot(_props: TDuplicateModalRootProps) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
|
||||
@@ -4,14 +4,11 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
|
||||
type TDeDupeIssueButtonLabelProps = {
|
||||
isOpen: boolean;
|
||||
buttonLabel: string;
|
||||
};
|
||||
|
||||
export function DeDupeIssueButtonLabel(props: TDeDupeIssueButtonLabelProps) {
|
||||
const { isOpen, buttonLabel } = props;
|
||||
export function DeDupeIssueButtonLabel(_props: TDeDupeIssueButtonLabelProps) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
import type { TIssue } from "@plane/types";
|
||||
|
||||
@@ -22,6 +21,6 @@ export interface EpicModalProps {
|
||||
isProjectSelectionDisabled?: boolean;
|
||||
}
|
||||
|
||||
export function CreateUpdateEpicModal(props: EpicModalProps) {
|
||||
export function CreateUpdateEpicModal(_props: EpicModalProps) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
|
||||
export type TEstimateTimeInputProps = {
|
||||
value?: number;
|
||||
handleEstimateInputValue: (value: string) => void;
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
|
||||
import type { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@plane/types";
|
||||
|
||||
export type TEstimatePointDelete = {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
type TUpdateEstimateModal = {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
//
|
||||
import type { IBlockUpdateDependencyData } from "@plane/types";
|
||||
import { GanttChartBlock } from "@/components/gantt-chart/blocks/block";
|
||||
|
||||
@@ -12,6 +12,6 @@ type LeftDependencyDraggableProps = {
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export function LeftDependencyDraggable(props: LeftDependencyDraggableProps) {
|
||||
export function LeftDependencyDraggable(_props: LeftDependencyDraggableProps) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ type RightDependencyDraggableProps = {
|
||||
block: IGanttBlock;
|
||||
ganttContainerRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
export function RightDependencyDraggable(props: RightDependencyDraggableProps) {
|
||||
export function RightDependencyDraggable(_props: RightDependencyDraggableProps) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,9 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
isEpic?: boolean;
|
||||
};
|
||||
export function TimelineDependencyPaths(props: Props) {
|
||||
const { isEpic = false } = props;
|
||||
export function TimelineDependencyPaths(_props: Props) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ export type TInboxSourcePill = {
|
||||
source: EInboxIssueSource;
|
||||
};
|
||||
|
||||
export function InboxSourcePill(props: TInboxSourcePill) {
|
||||
export function InboxSourcePill(_props: TInboxSourcePill) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
// plane types
|
||||
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
// plane types
|
||||
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
// plane types
|
||||
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export type TAdditionalActivityRoot = {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
// plane imports
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import Link from "next/link";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user