mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into feat/pagination
This commit is contained in:
commit
837193cda6
61
.github/workflows/build-branch.yml
vendored
61
.github/workflows/build-branch.yml
vendored
@ -23,6 +23,10 @@ jobs:
|
|||||||
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
|
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
|
||||||
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
|
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
|
||||||
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
|
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
|
||||||
|
build_frontend: ${{ steps.changed_files.outputs.frontend_any_changed }}
|
||||||
|
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
|
||||||
|
build_backend: ${{ steps.changed_files.outputs.backend_any_changed }}
|
||||||
|
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: set_env_variables
|
- id: set_env_variables
|
||||||
@ -41,7 +45,36 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- id: checkout_files
|
||||||
|
name: Checkout Files
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed_files
|
||||||
|
uses: tj-actions/changed-files@v42
|
||||||
|
with:
|
||||||
|
files_yaml: |
|
||||||
|
frontend:
|
||||||
|
- web/**
|
||||||
|
- packages/**
|
||||||
|
- 'package.json'
|
||||||
|
- 'yarn.lock'
|
||||||
|
- 'tsconfig.json'
|
||||||
|
- 'turbo.json'
|
||||||
|
space:
|
||||||
|
- space/**
|
||||||
|
- packages/**
|
||||||
|
- 'package.json'
|
||||||
|
- 'yarn.lock'
|
||||||
|
- 'tsconfig.json'
|
||||||
|
- 'turbo.json'
|
||||||
|
backend:
|
||||||
|
- apiserver/**
|
||||||
|
proxy:
|
||||||
|
- nginx/**
|
||||||
|
|
||||||
branch_build_push_frontend:
|
branch_build_push_frontend:
|
||||||
|
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
env:
|
env:
|
||||||
@ -55,9 +88,9 @@ jobs:
|
|||||||
- name: Set Frontend Docker Tag
|
- name: Set Frontend Docker Tag
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" == "release" ]; then
|
if [ "${{ github.event_name }}" == "release" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
|
||||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest
|
||||||
else
|
else
|
||||||
TAG=${{ env.FRONTEND_TAG }}
|
TAG=${{ env.FRONTEND_TAG }}
|
||||||
fi
|
fi
|
||||||
@ -77,7 +110,7 @@ jobs:
|
|||||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||||
|
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build and Push Frontend to Docker Container Registry
|
- name: Build and Push Frontend to Docker Container Registry
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
@ -93,6 +126,7 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
branch_build_push_space:
|
branch_build_push_space:
|
||||||
|
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
env:
|
env:
|
||||||
@ -106,9 +140,9 @@ jobs:
|
|||||||
- name: Set Space Docker Tag
|
- name: Set Space Docker Tag
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" == "release" ]; then
|
if [ "${{ github.event_name }}" == "release" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
|
||||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest
|
||||||
else
|
else
|
||||||
TAG=${{ env.SPACE_TAG }}
|
TAG=${{ env.SPACE_TAG }}
|
||||||
fi
|
fi
|
||||||
@ -128,7 +162,7 @@ jobs:
|
|||||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||||
|
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build and Push Space to Docker Hub
|
- name: Build and Push Space to Docker Hub
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
@ -144,6 +178,7 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
branch_build_push_backend:
|
branch_build_push_backend:
|
||||||
|
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
env:
|
env:
|
||||||
@ -157,9 +192,9 @@ jobs:
|
|||||||
- name: Set Backend Docker Tag
|
- name: Set Backend Docker Tag
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" == "release" ]; then
|
if [ "${{ github.event_name }}" == "release" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
|
||||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest
|
||||||
else
|
else
|
||||||
TAG=${{ env.BACKEND_TAG }}
|
TAG=${{ env.BACKEND_TAG }}
|
||||||
fi
|
fi
|
||||||
@ -179,7 +214,7 @@ jobs:
|
|||||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||||
|
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build and Push Backend to Docker Hub
|
- name: Build and Push Backend to Docker Hub
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
@ -194,8 +229,8 @@ jobs:
|
|||||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
branch_build_push_proxy:
|
branch_build_push_proxy:
|
||||||
|
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
env:
|
env:
|
||||||
@ -209,9 +244,9 @@ jobs:
|
|||||||
- name: Set Proxy Docker Tag
|
- name: Set Proxy Docker Tag
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" == "release" ]; then
|
if [ "${{ github.event_name }}" == "release" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
|
||||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
|
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest
|
||||||
else
|
else
|
||||||
TAG=${{ env.PROXY_TAG }}
|
TAG=${{ env.PROXY_TAG }}
|
||||||
fi
|
fi
|
||||||
@ -231,7 +266,7 @@ jobs:
|
|||||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||||
|
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build and Push Plane-Proxy to Docker Hub
|
- name: Build and Push Plane-Proxy to Docker Hub
|
||||||
uses: docker/build-push-action@v5.1.0
|
uses: docker/build-push-action@v5.1.0
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "plane-api",
|
"name": "plane-api",
|
||||||
"version": "0.15.1"
|
"version": "0.16.0"
|
||||||
}
|
}
|
||||||
|
@ -66,15 +66,15 @@ class ConfigurationEndpoint(BaseAPIView):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "SLACK_CLIENT_ID",
|
"key": "SLACK_CLIENT_ID",
|
||||||
"default": os.environ.get("SLACK_CLIENT_ID", "1"),
|
"default": os.environ.get("SLACK_CLIENT_ID", None),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "POSTHOG_API_KEY",
|
"key": "POSTHOG_API_KEY",
|
||||||
"default": os.environ.get("POSTHOG_API_KEY", "1"),
|
"default": os.environ.get("POSTHOG_API_KEY", None),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "POSTHOG_HOST",
|
"key": "POSTHOG_HOST",
|
||||||
"default": os.environ.get("POSTHOG_HOST", "1"),
|
"default": os.environ.get("POSTHOG_HOST", None),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "UNSPLASH_ACCESS_KEY",
|
"key": "UNSPLASH_ACCESS_KEY",
|
||||||
@ -181,11 +181,11 @@ class MobileConfigurationEndpoint(BaseAPIView):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "POSTHOG_API_KEY",
|
"key": "POSTHOG_API_KEY",
|
||||||
"default": os.environ.get("POSTHOG_API_KEY", "1"),
|
"default": os.environ.get("POSTHOG_API_KEY", None),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "POSTHOG_HOST",
|
"key": "POSTHOG_HOST",
|
||||||
"default": os.environ.get("POSTHOG_HOST", "1"),
|
"default": os.environ.get("POSTHOG_HOST", None),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "UNSPLASH_ACCESS_KEY",
|
"key": "UNSPLASH_ACCESS_KEY",
|
||||||
|
@ -33,6 +33,7 @@ from plane.app.serializers import (
|
|||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
InboxSerializer,
|
InboxSerializer,
|
||||||
InboxIssueSerializer,
|
InboxIssueSerializer,
|
||||||
|
IssueDetailSerializer,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
@ -426,11 +427,10 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if issue is None:
|
if issue is None:
|
||||||
return Response({"error": "Requested object was not found"}, status=status.HTTP_404_NOT_FOUND)
|
return Response({"error": "Requested object was not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
serializer = IssueSerializer(issue)
|
serializer = IssueDetailSerializer(issue)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, inbox_id, issue_id):
|
def destroy(self, request, slug, project_id, inbox_id, issue_id):
|
||||||
|
@ -2018,16 +2018,9 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
serializer = IssueCreateSerializer(issue, data=request.data, partial=True)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if request.data.get(
|
|
||||||
"is_draft"
|
|
||||||
) is not None and not request.data.get("is_draft"):
|
|
||||||
serializer.save(
|
|
||||||
created_at=timezone.now(), updated_at=timezone.now()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue_draft.activity.updated",
|
type="issue_draft.activity.updated",
|
||||||
|
@ -77,6 +77,12 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
sort_order = ProjectMember.objects.filter(
|
||||||
|
member=self.request.user,
|
||||||
|
project_id=OuterRef("pk"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
|
).values("sort_order")
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
@ -147,6 +153,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.annotate(sort_order=Subquery(sort_order))
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"project_projectmember",
|
"project_projectmember",
|
||||||
@ -166,16 +173,8 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
for field in request.GET.get("fields", "").split(",")
|
for field in request.GET.get("fields", "").split(",")
|
||||||
if field
|
if field
|
||||||
]
|
]
|
||||||
|
|
||||||
sort_order_query = ProjectMember.objects.filter(
|
|
||||||
member=request.user,
|
|
||||||
project_id=OuterRef("pk"),
|
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
|
||||||
is_active=True,
|
|
||||||
).values("sort_order")
|
|
||||||
projects = (
|
projects = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
.annotate(sort_order=Subquery(sort_order_query))
|
|
||||||
.order_by("sort_order", "name")
|
.order_by("sort_order", "name")
|
||||||
)
|
)
|
||||||
if request.GET.get("per_page", False) and request.GET.get(
|
if request.GET.get("per_page", False) and request.GET.get(
|
||||||
@ -204,7 +203,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
# Add the user as Administrator to the project
|
# Add the user as Administrator to the project
|
||||||
project_member = ProjectMember.objects.create(
|
_ = ProjectMember.objects.create(
|
||||||
project_id=serializer.data["id"],
|
project_id=serializer.data["id"],
|
||||||
member=request.user,
|
member=request.user,
|
||||||
role=20,
|
role=20,
|
||||||
|
@ -9,11 +9,11 @@ from plane.db.models import Issue
|
|||||||
|
|
||||||
|
|
||||||
def search_issues(query, queryset):
|
def search_issues(query, queryset):
|
||||||
fields = ["name", "sequence_id"]
|
fields = ["name", "sequence_id", "project__identifier"]
|
||||||
q = Q()
|
q = Q()
|
||||||
for field in fields:
|
for field in fields:
|
||||||
if field == "sequence_id" and len(query) <= 20:
|
if field == "sequence_id" and len(query) <= 20:
|
||||||
sequences = re.findall(r"[A-Za-z0-9]{1,12}-\d+", query)
|
sequences = re.findall(r"\b\d+\b", query)
|
||||||
for sequence_id in sequences:
|
for sequence_id in sequences:
|
||||||
q |= Q(**{"sequence_id": sequence_id})
|
q |= Q(**{"sequence_id": sequence_id})
|
||||||
else:
|
else:
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
export GIT_REPO=makeplane/plane
|
||||||
|
|
||||||
# Check if the user has sudo access
|
# Check if the user has sudo access
|
||||||
if command -v curl &> /dev/null; then
|
if command -v curl &> /dev/null; then
|
||||||
sudo curl -sSL \
|
sudo curl -sSL \
|
||||||
-o /usr/local/bin/plane-app \
|
-o /usr/local/bin/plane-app \
|
||||||
https://raw.githubusercontent.com/makeplane/plane/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s)
|
https://raw.githubusercontent.com/$GIT_REPO/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s)
|
||||||
else
|
else
|
||||||
sudo wget -q \
|
sudo wget -q \
|
||||||
-O /usr/local/bin/plane-app \
|
-O /usr/local/bin/plane-app \
|
||||||
https://raw.githubusercontent.com/makeplane/plane/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s)
|
https://raw.githubusercontent.com/$GIT_REPO/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo chmod +x /usr/local/bin/plane-app
|
sudo chmod +x /usr/local/bin/plane-app
|
||||||
sudo sed -i 's/export DEPLOY_BRANCH=${BRANCH:-master}/export DEPLOY_BRANCH='${BRANCH:-master}'/' /usr/local/bin/plane-app
|
sudo sed -i 's@export DEPLOY_BRANCH=${BRANCH:-master}@export DEPLOY_BRANCH='${BRANCH:-master}'@' /usr/local/bin/plane-app
|
||||||
|
sudo sed -i 's@CODE_REPO=${GIT_REPO:-makeplane/plane}@CODE_REPO='$GIT_REPO'@' /usr/local/bin/plane-app
|
||||||
|
|
||||||
plane-app --help
|
plane-app -i #--help
|
||||||
|
@ -90,9 +90,9 @@ function prepare_environment() {
|
|||||||
|
|
||||||
show_message "- Updating OS with required tools ✋" >&2
|
show_message "- Updating OS with required tools ✋" >&2
|
||||||
sudo "$PACKAGE_MANAGER" update -y
|
sudo "$PACKAGE_MANAGER" update -y
|
||||||
sudo "$PACKAGE_MANAGER" upgrade -y
|
# sudo "$PACKAGE_MANAGER" upgrade -y
|
||||||
|
|
||||||
local required_tools=("curl" "awk" "wget" "nano" "dialog" "git" "uidmap")
|
local required_tools=("curl" "awk" "wget" "nano" "dialog" "git" "uidmap" "jq")
|
||||||
|
|
||||||
for tool in "${required_tools[@]}"; do
|
for tool in "${required_tools[@]}"; do
|
||||||
if ! command -v $tool &> /dev/null; then
|
if ! command -v $tool &> /dev/null; then
|
||||||
@ -150,11 +150,11 @@ function download_plane() {
|
|||||||
show_message "Downloading Plane Setup Files ✋" >&2
|
show_message "Downloading Plane Setup Files ✋" >&2
|
||||||
sudo curl -H 'Cache-Control: no-cache, no-store' \
|
sudo curl -H 'Cache-Control: no-cache, no-store' \
|
||||||
-s -o $PLANE_INSTALL_DIR/docker-compose.yaml \
|
-s -o $PLANE_INSTALL_DIR/docker-compose.yaml \
|
||||||
https://raw.githubusercontent.com/makeplane/plane/$DEPLOY_BRANCH/deploy/selfhost/docker-compose.yml?token=$(date +%s)
|
https://raw.githubusercontent.com/$CODE_REPO/$DEPLOY_BRANCH/deploy/selfhost/docker-compose.yml?token=$(date +%s)
|
||||||
|
|
||||||
sudo curl -H 'Cache-Control: no-cache, no-store' \
|
sudo curl -H 'Cache-Control: no-cache, no-store' \
|
||||||
-s -o $PLANE_INSTALL_DIR/variables-upgrade.env \
|
-s -o $PLANE_INSTALL_DIR/variables-upgrade.env \
|
||||||
https://raw.githubusercontent.com/makeplane/plane/$DEPLOY_BRANCH/deploy/selfhost/variables.env?token=$(date +%s)
|
https://raw.githubusercontent.com/$CODE_REPO/$DEPLOY_BRANCH/deploy/selfhost/variables.env?token=$(date +%s)
|
||||||
|
|
||||||
# if .env does not exists rename variables-upgrade.env to .env
|
# if .env does not exists rename variables-upgrade.env to .env
|
||||||
if [ ! -f "$PLANE_INSTALL_DIR/.env" ]; then
|
if [ ! -f "$PLANE_INSTALL_DIR/.env" ]; then
|
||||||
@ -202,7 +202,7 @@ function printUsageInstructions() {
|
|||||||
}
|
}
|
||||||
function build_local_image() {
|
function build_local_image() {
|
||||||
show_message "- Downloading Plane Source Code ✋" >&2
|
show_message "- Downloading Plane Source Code ✋" >&2
|
||||||
REPO=https://github.com/makeplane/plane.git
|
REPO=https://github.com/$CODE_REPO.git
|
||||||
CURR_DIR=$PWD
|
CURR_DIR=$PWD
|
||||||
PLANE_TEMP_CODE_DIR=$PLANE_INSTALL_DIR/temp
|
PLANE_TEMP_CODE_DIR=$PLANE_INSTALL_DIR/temp
|
||||||
sudo rm -rf $PLANE_TEMP_CODE_DIR > /dev/null
|
sudo rm -rf $PLANE_TEMP_CODE_DIR > /dev/null
|
||||||
@ -290,40 +290,40 @@ function configure_plane() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
smtp_host=$(read_env "EMAIL_HOST")
|
# smtp_host=$(read_env "EMAIL_HOST")
|
||||||
smtp_user=$(read_env "EMAIL_HOST_USER")
|
# smtp_user=$(read_env "EMAIL_HOST_USER")
|
||||||
smtp_password=$(read_env "EMAIL_HOST_PASSWORD")
|
# smtp_password=$(read_env "EMAIL_HOST_PASSWORD")
|
||||||
smtp_port=$(read_env "EMAIL_PORT")
|
# smtp_port=$(read_env "EMAIL_PORT")
|
||||||
smtp_from=$(read_env "EMAIL_FROM")
|
# smtp_from=$(read_env "EMAIL_FROM")
|
||||||
smtp_tls=$(read_env "EMAIL_USE_TLS")
|
# smtp_tls=$(read_env "EMAIL_USE_TLS")
|
||||||
smtp_ssl=$(read_env "EMAIL_USE_SSL")
|
# smtp_ssl=$(read_env "EMAIL_USE_SSL")
|
||||||
|
|
||||||
SMTP_SETTINGS=$(dialog \
|
# SMTP_SETTINGS=$(dialog \
|
||||||
--ok-label "Next" \
|
# --ok-label "Next" \
|
||||||
--cancel-label "Skip" \
|
# --cancel-label "Skip" \
|
||||||
--backtitle "Plane Configuration" \
|
# --backtitle "Plane Configuration" \
|
||||||
--title "SMTP Settings" \
|
# --title "SMTP Settings" \
|
||||||
--form "" \
|
# --form "" \
|
||||||
0 0 0 \
|
# 0 0 0 \
|
||||||
"Host:" 1 1 "$smtp_host" 1 10 80 0 \
|
# "Host:" 1 1 "$smtp_host" 1 10 80 0 \
|
||||||
"User:" 2 1 "$smtp_user" 2 10 80 0 \
|
# "User:" 2 1 "$smtp_user" 2 10 80 0 \
|
||||||
"Password:" 3 1 "$smtp_password" 3 10 80 0 \
|
# "Password:" 3 1 "$smtp_password" 3 10 80 0 \
|
||||||
"Port:" 4 1 "${smtp_port:-587}" 4 10 5 0 \
|
# "Port:" 4 1 "${smtp_port:-587}" 4 10 5 0 \
|
||||||
"From:" 5 1 "${smtp_from:-Mailer <mailer@example.com>}" 5 10 80 0 \
|
# "From:" 5 1 "${smtp_from:-Mailer <mailer@example.com>}" 5 10 80 0 \
|
||||||
"TLS:" 6 1 "${smtp_tls:-1}" 6 10 1 1 \
|
# "TLS:" 6 1 "${smtp_tls:-1}" 6 10 1 1 \
|
||||||
"SSL:" 7 1 "${smtp_ssl:-0}" 7 10 1 1 \
|
# "SSL:" 7 1 "${smtp_ssl:-0}" 7 10 1 1 \
|
||||||
2>&1 1>&3)
|
# 2>&1 1>&3)
|
||||||
|
|
||||||
save_smtp_settings=0
|
# save_smtp_settings=0
|
||||||
if [ $? -eq 0 ]; then
|
# if [ $? -eq 0 ]; then
|
||||||
save_smtp_settings=1
|
# save_smtp_settings=1
|
||||||
smtp_host=$(echo "$SMTP_SETTINGS" | sed -n 1p)
|
# smtp_host=$(echo "$SMTP_SETTINGS" | sed -n 1p)
|
||||||
smtp_user=$(echo "$SMTP_SETTINGS" | sed -n 2p)
|
# smtp_user=$(echo "$SMTP_SETTINGS" | sed -n 2p)
|
||||||
smtp_password=$(echo "$SMTP_SETTINGS" | sed -n 3p)
|
# smtp_password=$(echo "$SMTP_SETTINGS" | sed -n 3p)
|
||||||
smtp_port=$(echo "$SMTP_SETTINGS" | sed -n 4p)
|
# smtp_port=$(echo "$SMTP_SETTINGS" | sed -n 4p)
|
||||||
smtp_from=$(echo "$SMTP_SETTINGS" | sed -n 5p)
|
# smtp_from=$(echo "$SMTP_SETTINGS" | sed -n 5p)
|
||||||
smtp_tls=$(echo "$SMTP_SETTINGS" | sed -n 6p)
|
# smtp_tls=$(echo "$SMTP_SETTINGS" | sed -n 6p)
|
||||||
fi
|
# fi
|
||||||
external_pgdb_url=$(dialog \
|
external_pgdb_url=$(dialog \
|
||||||
--backtitle "Plane Configuration" \
|
--backtitle "Plane Configuration" \
|
||||||
--title "Using External Postgres Database ?" \
|
--title "Using External Postgres Database ?" \
|
||||||
@ -383,15 +383,6 @@ function configure_plane() {
|
|||||||
domain_name: $domain_name
|
domain_name: $domain_name
|
||||||
upload_limit: $upload_limit
|
upload_limit: $upload_limit
|
||||||
|
|
||||||
save_smtp_settings: $save_smtp_settings
|
|
||||||
smtp_host: $smtp_host
|
|
||||||
smtp_user: $smtp_user
|
|
||||||
smtp_password: $smtp_password
|
|
||||||
smtp_port: $smtp_port
|
|
||||||
smtp_from: $smtp_from
|
|
||||||
smtp_tls: $smtp_tls
|
|
||||||
smtp_ssl: $smtp_ssl
|
|
||||||
|
|
||||||
save_aws_settings: $save_aws_settings
|
save_aws_settings: $save_aws_settings
|
||||||
aws_region: $aws_region
|
aws_region: $aws_region
|
||||||
aws_access_key: $aws_access_key
|
aws_access_key: $aws_access_key
|
||||||
@ -413,15 +404,15 @@ function configure_plane() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# check enable smpt settings value
|
# check enable smpt settings value
|
||||||
if [ $save_smtp_settings == 1 ]; then
|
# if [ $save_smtp_settings == 1 ]; then
|
||||||
update_env "EMAIL_HOST" "$smtp_host"
|
# update_env "EMAIL_HOST" "$smtp_host"
|
||||||
update_env "EMAIL_HOST_USER" "$smtp_user"
|
# update_env "EMAIL_HOST_USER" "$smtp_user"
|
||||||
update_env "EMAIL_HOST_PASSWORD" "$smtp_password"
|
# update_env "EMAIL_HOST_PASSWORD" "$smtp_password"
|
||||||
update_env "EMAIL_PORT" "$smtp_port"
|
# update_env "EMAIL_PORT" "$smtp_port"
|
||||||
update_env "EMAIL_FROM" "$smtp_from"
|
# update_env "EMAIL_FROM" "$smtp_from"
|
||||||
update_env "EMAIL_USE_TLS" "$smtp_tls"
|
# update_env "EMAIL_USE_TLS" "$smtp_tls"
|
||||||
update_env "EMAIL_USE_SSL" "$smtp_ssl"
|
# update_env "EMAIL_USE_SSL" "$smtp_ssl"
|
||||||
fi
|
# fi
|
||||||
|
|
||||||
# check enable aws settings value
|
# check enable aws settings value
|
||||||
if [[ $save_aws_settings == 1 && $aws_access_key != "" && $aws_secret_key != "" ]] ; then
|
if [[ $save_aws_settings == 1 && $aws_access_key != "" && $aws_secret_key != "" ]] ; then
|
||||||
@ -493,12 +484,23 @@ function install() {
|
|||||||
check_for_docker_images
|
check_for_docker_images
|
||||||
|
|
||||||
last_installed_on=$(read_config "INSTALLATION_DATE")
|
last_installed_on=$(read_config "INSTALLATION_DATE")
|
||||||
if [ "$last_installed_on" == "" ]; then
|
# if [ "$last_installed_on" == "" ]; then
|
||||||
configure_plane
|
# configure_plane
|
||||||
fi
|
# fi
|
||||||
printUsageInstructions
|
|
||||||
|
|
||||||
update_config "INSTALLATION_DATE" "$(date)"
|
update_env "NGINX_PORT" "80"
|
||||||
|
update_env "DOMAIN_NAME" "$MY_IP"
|
||||||
|
update_env "WEB_URL" "http://$MY_IP"
|
||||||
|
update_env "CORS_ALLOWED_ORIGINS" "http://$MY_IP"
|
||||||
|
|
||||||
|
update_config "INSTALLATION_DATE" "$(date '+%Y-%m-%d')"
|
||||||
|
|
||||||
|
if command -v crontab &> /dev/null; then
|
||||||
|
sudo touch /etc/cron.daily/makeplane
|
||||||
|
sudo chmod +x /etc/cron.daily/makeplane
|
||||||
|
sudo echo "0 2 * * * root /usr/local/bin/plane-app --upgrade" > /etc/cron.daily/makeplane
|
||||||
|
sudo crontab /etc/cron.daily/makeplane
|
||||||
|
fi
|
||||||
|
|
||||||
show_message "Plane Installed Successfully ✅"
|
show_message "Plane Installed Successfully ✅"
|
||||||
show_message ""
|
show_message ""
|
||||||
@ -539,12 +541,15 @@ function upgrade() {
|
|||||||
prepare_environment
|
prepare_environment
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
|
stop_server
|
||||||
download_plane
|
download_plane
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
check_for_docker_images
|
check_for_docker_images
|
||||||
upgrade_configuration
|
upgrade_configuration
|
||||||
update_config "UPGRADE_DATE" "$(date)"
|
update_config "UPGRADE_DATE" "$(date)"
|
||||||
|
|
||||||
|
start_server
|
||||||
|
|
||||||
show_message ""
|
show_message ""
|
||||||
show_message "Plane Upgraded Successfully ✅"
|
show_message "Plane Upgraded Successfully ✅"
|
||||||
show_message ""
|
show_message ""
|
||||||
@ -602,6 +607,11 @@ function uninstall() {
|
|||||||
sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null
|
sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null
|
||||||
sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null
|
sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null
|
||||||
|
|
||||||
|
if command -v crontab &> /dev/null; then
|
||||||
|
sudo crontab -r &> /dev/null
|
||||||
|
sudo rm /etc/cron.daily/makeplane &> /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
# rm -rf $PLANE_INSTALL_DIR &> /dev/null
|
# rm -rf $PLANE_INSTALL_DIR &> /dev/null
|
||||||
show_message "- Configuration Cleaned ✅"
|
show_message "- Configuration Cleaned ✅"
|
||||||
|
|
||||||
@ -642,7 +652,39 @@ function start_server() {
|
|||||||
while ! sudo docker compose -f "$docker_compose_file" --env-file="$env_file" ps --services --filter "status=running" --quiet | grep -q "."; do
|
while ! sudo docker compose -f "$docker_compose_file" --env-file="$env_file" ps --services --filter "status=running" --quiet | grep -q "."; do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
# wait for migrator container to exit with status 0 before starting the application
|
||||||
|
migrator_container_id=$(sudo docker container ls -aq -f "name=plane-migrator")
|
||||||
|
|
||||||
|
# if migrator container is running, wait for it to exit
|
||||||
|
if [ -n "$migrator_container_id" ]; then
|
||||||
|
while sudo docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
|
||||||
|
show_message "Waiting for Plane Server ($APP_RELEASE) to start...✋ (Migrator in progress)" "replace_last_line" >&2
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# if migrator exit status is not 0, show error message and exit
|
||||||
|
if [ -n "$migrator_container_id" ]; then
|
||||||
|
migrator_exit_code=$(sudo docker inspect --format='{{.State.ExitCode}}' $migrator_container_id)
|
||||||
|
if [ $migrator_exit_code -ne 0 ]; then
|
||||||
|
# show_message "Migrator failed with exit code $migrator_exit_code ❌" "replace_last_line" >&2
|
||||||
|
show_message "Plane Server failed to start ❌" "replace_last_line" >&2
|
||||||
|
stop_server
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
api_container_id=$(sudo docker container ls -q -f "name=plane-api")
|
||||||
|
while ! sudo docker logs $api_container_id 2>&1 | grep -i "Application startup complete";
|
||||||
|
do
|
||||||
|
show_message "Waiting for Plane Server ($APP_RELEASE) to start...✋ (API starting)" "replace_last_line" >&2
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
show_message "Plane Server Started ($APP_RELEASE) ✅" "replace_last_line" >&2
|
show_message "Plane Server Started ($APP_RELEASE) ✅" "replace_last_line" >&2
|
||||||
|
show_message "---------------------------------------------------------------" >&2
|
||||||
|
show_message "Access the Plane application at http://$MY_IP" >&2
|
||||||
|
show_message "---------------------------------------------------------------" >&2
|
||||||
|
|
||||||
else
|
else
|
||||||
show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2
|
show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2
|
||||||
fi
|
fi
|
||||||
@ -694,7 +736,7 @@ function update_installer() {
|
|||||||
show_message "Updating Plane Installer ✋" >&2
|
show_message "Updating Plane Installer ✋" >&2
|
||||||
sudo curl -H 'Cache-Control: no-cache, no-store' \
|
sudo curl -H 'Cache-Control: no-cache, no-store' \
|
||||||
-s -o /usr/local/bin/plane-app \
|
-s -o /usr/local/bin/plane-app \
|
||||||
https://raw.githubusercontent.com/makeplane/plane/$DEPLOY_BRANCH/deploy/1-click/plane-app?token=$(date +%s)
|
https://raw.githubusercontent.com/$CODE_REPO/$DEPLOY_BRANCH/deploy/1-click/plane-app?token=$(date +%s)
|
||||||
|
|
||||||
sudo chmod +x /usr/local/bin/plane-app > /dev/null&> /dev/null
|
sudo chmod +x /usr/local/bin/plane-app > /dev/null&> /dev/null
|
||||||
show_message "Plane Installer Updated ✅" "replace_last_line" >&2
|
show_message "Plane Installer Updated ✅" "replace_last_line" >&2
|
||||||
@ -711,12 +753,14 @@ fi
|
|||||||
|
|
||||||
PLANE_INSTALL_DIR=/opt/plane
|
PLANE_INSTALL_DIR=/opt/plane
|
||||||
DATA_DIR=$PLANE_INSTALL_DIR/data
|
DATA_DIR=$PLANE_INSTALL_DIR/data
|
||||||
LOG_DIR=$PLANE_INSTALL_DIR/log
|
LOG_DIR=$PLANE_INSTALL_DIR/logs
|
||||||
|
CODE_REPO=${GIT_REPO:-makeplane/plane}
|
||||||
OS_SUPPORTED=false
|
OS_SUPPORTED=false
|
||||||
CPU_ARCH=$(uname -m)
|
CPU_ARCH=$(uname -m)
|
||||||
PROGRESS_MSG=""
|
PROGRESS_MSG=""
|
||||||
USE_GLOBAL_IMAGES=0
|
USE_GLOBAL_IMAGES=0
|
||||||
PACKAGE_MANAGER=""
|
PACKAGE_MANAGER=""
|
||||||
|
MY_IP=$(curl -s ifconfig.me)
|
||||||
|
|
||||||
if [[ $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $DEPLOY_BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]]; then
|
if [[ $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $DEPLOY_BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]]; then
|
||||||
USE_GLOBAL_IMAGES=1
|
USE_GLOBAL_IMAGES=1
|
||||||
@ -740,6 +784,9 @@ elif [ "$1" == "restart" ]; then
|
|||||||
restart_server
|
restart_server
|
||||||
elif [ "$1" == "--install" ] || [ "$1" == "-i" ]; then
|
elif [ "$1" == "--install" ] || [ "$1" == "-i" ]; then
|
||||||
install
|
install
|
||||||
|
start_server
|
||||||
|
show_message "" >&2
|
||||||
|
show_message "To view help, use plane-app --help " >&2
|
||||||
elif [ "$1" == "--configure" ] || [ "$1" == "-c" ]; then
|
elif [ "$1" == "--configure" ] || [ "$1" == "-c" ]; then
|
||||||
configure_plane
|
configure_plane
|
||||||
printUsageInstructions
|
printUsageInstructions
|
||||||
|
@ -56,8 +56,6 @@ x-app-env : &app-env
|
|||||||
- BUCKET_NAME=${BUCKET_NAME:-uploads}
|
- BUCKET_NAME=${BUCKET_NAME:-uploads}
|
||||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
<<: *app-env
|
<<: *app-env
|
||||||
@ -138,7 +136,6 @@ services:
|
|||||||
command: postgres -c 'max_connections=1000'
|
command: postgres -c 'max_connections=1000'
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
plane-redis:
|
plane-redis:
|
||||||
<<: *app-env
|
<<: *app-env
|
||||||
image: redis:6.2.7-alpine
|
image: redis:6.2.7-alpine
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"repository": "https://github.com/makeplane/plane.git",
|
"repository": "https://github.com/makeplane/plane.git",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/editor-core",
|
"name": "@plane/editor-core",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"description": "Core Editor that powers Plane",
|
"description": "Core Editor that powers Plane",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/document-editor",
|
"name": "@plane/document-editor",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"description": "Package that powers Plane's Pages Editor",
|
"description": "Package that powers Plane's Pages Editor",
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/editor-extensions",
|
"name": "@plane/editor-extensions",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"description": "Package that powers Plane's Editor with extensions",
|
"description": "Package that powers Plane's Editor with extensions",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/lite-text-editor",
|
"name": "@plane/lite-text-editor",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"description": "Package that powers Plane's Comment Editor",
|
"description": "Package that powers Plane's Comment Editor",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/rich-text-editor",
|
"name": "@plane/rich-text-editor",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"description": "Rich Text Editor that powers Plane",
|
"description": "Rich Text Editor that powers Plane",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "eslint-config-custom",
|
"name": "eslint-config-custom",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tailwind-config-custom",
|
"name": "tailwind-config-custom",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"description": "common tailwind configuration across monorepo",
|
"description": "common tailwind configuration across monorepo",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tsconfig",
|
"name": "tsconfig",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"files": [
|
"files": [
|
||||||
"base.json",
|
"base.json",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@plane/types",
|
"name": "@plane/types",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./src/index.d.ts"
|
"main": "./src/index.d.ts"
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "@plane/ui",
|
"name": "@plane/ui",
|
||||||
"description": "UI components shared across multiple apps internally",
|
"description": "UI components shared across multiple apps internally",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "space",
|
"name": "space",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo run develop",
|
"dev": "turbo run develop",
|
||||||
|
@ -67,6 +67,7 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
|
|
||||||
const filterParams = getRedirectionFilters(selectedTab);
|
const filterParams = getRedirectionFilters(selectedTab);
|
||||||
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
|
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
|
||||||
|
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
|
||||||
|
|
||||||
if (!widgetDetails || !widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
|
if (!widgetDetails || !widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
|
||||||
|
|
||||||
@ -84,30 +85,25 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val === selectedDurationFilter) return;
|
if (val === selectedDurationFilter) return;
|
||||||
|
|
||||||
|
let newTab = selectedTab;
|
||||||
// switch to pending tab if target date is changed to none
|
// switch to pending tab if target date is changed to none
|
||||||
if (val === "none" && selectedTab !== "completed") {
|
if (val === "none" && selectedTab !== "completed") newTab = "pending";
|
||||||
handleUpdateFilters({ duration: val, tab: "pending" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// switch to upcoming tab if target date is changed to other than none
|
// switch to upcoming tab if target date is changed to other than none
|
||||||
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
|
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") newTab = "upcoming";
|
||||||
|
|
||||||
handleUpdateFilters({
|
handleUpdateFilters({
|
||||||
duration: val,
|
duration: val,
|
||||||
tab: "upcoming",
|
tab: newTab,
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUpdateFilters({ duration: val });
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Tab.Group
|
<Tab.Group
|
||||||
as="div"
|
as="div"
|
||||||
selectedIndex={tabsList.findIndex((tab) => tab.key === selectedTab)}
|
selectedIndex={selectedTabIndex}
|
||||||
onChange={(i) => {
|
onChange={(i) => {
|
||||||
const selectedTab = tabsList[i];
|
const newSelectedTab = tabsList[i];
|
||||||
handleUpdateFilters({ tab: selectedTab?.key ?? "pending" });
|
handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" });
|
||||||
}}
|
}}
|
||||||
className="h-full flex flex-col"
|
className="h-full flex flex-col"
|
||||||
>
|
>
|
||||||
@ -115,18 +111,21 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
|
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
|
||||||
</div>
|
</div>
|
||||||
<Tab.Panels as="div" className="h-full">
|
<Tab.Panels as="div" className="h-full">
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => {
|
||||||
<Tab.Panel key={tab.key} as="div" className="h-full flex flex-col">
|
if (tab.key !== selectedTab) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab.Panel key={tab.key} as="div" className="h-full flex flex-col" static>
|
||||||
<WidgetIssuesList
|
<WidgetIssuesList
|
||||||
issues={widgetStats.issues}
|
|
||||||
tab={tab.key}
|
tab={tab.key}
|
||||||
totalIssues={widgetStats.count}
|
|
||||||
type="assigned"
|
type="assigned"
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
|
widgetStats={widgetStats}
|
||||||
isLoading={fetching}
|
isLoading={fetching}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,6 +64,7 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
|
|
||||||
const filterParams = getRedirectionFilters(selectedTab);
|
const filterParams = getRedirectionFilters(selectedTab);
|
||||||
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
|
const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
|
||||||
|
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
|
||||||
|
|
||||||
if (!widgetDetails || !widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
|
if (!widgetDetails || !widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
|
||||||
|
|
||||||
@ -81,30 +82,25 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val === selectedDurationFilter) return;
|
if (val === selectedDurationFilter) return;
|
||||||
|
|
||||||
|
let newTab = selectedTab;
|
||||||
// switch to pending tab if target date is changed to none
|
// switch to pending tab if target date is changed to none
|
||||||
if (val === "none" && selectedTab !== "completed") {
|
if (val === "none" && selectedTab !== "completed") newTab = "pending";
|
||||||
handleUpdateFilters({ duration: val, tab: "pending" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// switch to upcoming tab if target date is changed to other than none
|
// switch to upcoming tab if target date is changed to other than none
|
||||||
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
|
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") newTab = "upcoming";
|
||||||
|
|
||||||
handleUpdateFilters({
|
handleUpdateFilters({
|
||||||
duration: val,
|
duration: val,
|
||||||
tab: "upcoming",
|
tab: newTab,
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUpdateFilters({ duration: val });
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Tab.Group
|
<Tab.Group
|
||||||
as="div"
|
as="div"
|
||||||
selectedIndex={tabsList.findIndex((tab) => tab.key === selectedTab)}
|
selectedIndex={selectedTabIndex}
|
||||||
onChange={(i) => {
|
onChange={(i) => {
|
||||||
const selectedTab = tabsList[i];
|
const newSelectedTab = tabsList[i];
|
||||||
handleUpdateFilters({ tab: selectedTab.key ?? "pending" });
|
handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" });
|
||||||
}}
|
}}
|
||||||
className="h-full flex flex-col"
|
className="h-full flex flex-col"
|
||||||
>
|
>
|
||||||
@ -112,18 +108,21 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
|
|||||||
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
|
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
|
||||||
</div>
|
</div>
|
||||||
<Tab.Panels as="div" className="h-full">
|
<Tab.Panels as="div" className="h-full">
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => {
|
||||||
<Tab.Panel key={tab.key} as="div" className="h-full flex flex-col">
|
if (tab.key !== selectedTab) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab.Panel key={tab.key} as="div" className="h-full flex flex-col" static>
|
||||||
<WidgetIssuesList
|
<WidgetIssuesList
|
||||||
issues={widgetStats.issues}
|
|
||||||
tab={tab.key}
|
tab={tab.key}
|
||||||
totalIssues={widgetStats.count}
|
|
||||||
type="created"
|
type="created"
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
|
widgetStats={widgetStats}
|
||||||
isLoading={fetching}
|
isLoading={fetching}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,19 +19,18 @@ import { Loader, getButtonStyling } from "@plane/ui";
|
|||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
import { getRedirectionFilters } from "helpers/dashboard.helper";
|
import { getRedirectionFilters } from "helpers/dashboard.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue, TIssuesListTypes } from "@plane/types";
|
import { TAssignedIssuesWidgetResponse, TCreatedIssuesWidgetResponse, TIssue, TIssuesListTypes } from "@plane/types";
|
||||||
|
|
||||||
export type WidgetIssuesListProps = {
|
export type WidgetIssuesListProps = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
issues: TIssue[];
|
|
||||||
tab: TIssuesListTypes;
|
tab: TIssuesListTypes;
|
||||||
totalIssues: number;
|
|
||||||
type: "assigned" | "created";
|
type: "assigned" | "created";
|
||||||
|
widgetStats: TAssignedIssuesWidgetResponse | TCreatedIssuesWidgetResponse;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
||||||
const { isLoading, issues, tab, totalIssues, type, workspaceSlug } = props;
|
const { isLoading, tab, type, widgetStats, workspaceSlug } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { setPeekIssue } = useIssueDetail();
|
const { setPeekIssue } = useIssueDetail();
|
||||||
|
|
||||||
@ -59,6 +58,8 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const issuesList = widgetStats.issues;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
@ -69,7 +70,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
|||||||
<Loader.Item height="25px" />
|
<Loader.Item height="25px" />
|
||||||
<Loader.Item height="25px" />
|
<Loader.Item height="25px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
) : issues.length > 0 ? (
|
) : issuesList.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="mt-7 mx-6 border-b-[0.5px] border-custom-border-200 grid grid-cols-6 gap-1 text-xs text-custom-text-300 pb-1">
|
<div className="mt-7 mx-6 border-b-[0.5px] border-custom-border-200 grid grid-cols-6 gap-1 text-xs text-custom-text-300 pb-1">
|
||||||
<h6
|
<h6
|
||||||
@ -80,7 +81,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
|||||||
>
|
>
|
||||||
Issues
|
Issues
|
||||||
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl px-3 flex items-center text-center justify-center">
|
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl px-3 flex items-center text-center justify-center">
|
||||||
{totalIssues}
|
{widgetStats.count}
|
||||||
</span>
|
</span>
|
||||||
</h6>
|
</h6>
|
||||||
{["upcoming", "pending"].includes(tab) && <h6 className="text-center">Due date</h6>}
|
{["upcoming", "pending"].includes(tab) && <h6 className="text-center">Due date</h6>}
|
||||||
@ -89,7 +90,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
|||||||
{type === "created" && <h6 className="text-center">Assigned to</h6>}
|
{type === "created" && <h6 className="text-center">Assigned to</h6>}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 pb-3 mt-2">
|
<div className="px-4 pb-3 mt-2">
|
||||||
{issues.map((issue) => {
|
{issuesList.map((issue) => {
|
||||||
const IssueListItem = ISSUE_LIST_ITEM[type][tab];
|
const IssueListItem = ISSUE_LIST_ITEM[type][tab];
|
||||||
|
|
||||||
if (!IssueListItem) return null;
|
if (!IssueListItem) return null;
|
||||||
@ -112,7 +113,7 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{issues.length > 0 && (
|
{!isLoading && issuesList.length > 0 && (
|
||||||
<Link
|
<Link
|
||||||
href={`/${workspaceSlug}/workspace-views/${type}/${filterParams}`}
|
href={`/${workspaceSlug}/workspace-views/${type}/${filterParams}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
@ -148,6 +148,13 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (query !== "" && e.key === "Escape") {
|
||||||
|
e.stopPropagation();
|
||||||
|
setQuery("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -231,6 +238,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
onKeyDown={searchInputKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
|
@ -137,6 +137,13 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (query !== "" && e.key === "Escape") {
|
||||||
|
e.stopPropagation();
|
||||||
|
setQuery("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -217,6 +224,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
onKeyDown={searchInputKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
|
@ -130,6 +130,13 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (query !== "" && e.key === "Escape") {
|
||||||
|
e.stopPropagation();
|
||||||
|
setQuery("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -215,6 +222,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
onKeyDown={searchInputKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
|
@ -249,6 +249,13 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (query !== "" && e.key === "Escape") {
|
||||||
|
e.stopPropagation();
|
||||||
|
setQuery("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
const comboboxProps: any = {
|
const comboboxProps: any = {
|
||||||
@ -349,6 +356,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
onKeyDown={searchInputKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
|
@ -329,6 +329,13 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (query !== "" && e.key === "Escape") {
|
||||||
|
e.stopPropagation();
|
||||||
|
setQuery("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant)
|
const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant)
|
||||||
@ -417,6 +424,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
onKeyDown={searchInputKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
|
@ -119,6 +119,13 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (query !== "" && e.key === "Escape") {
|
||||||
|
e.stopPropagation();
|
||||||
|
setQuery("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleClose);
|
useOutsideClickDetector(dropdownRef, handleClose);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -205,6 +212,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
onKeyDown={searchInputKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
|
@ -15,9 +15,9 @@ export const MonthChartView: FC<any> = observer(() => {
|
|||||||
const monthBlocks: IMonthBlock[] = renderView;
|
const monthBlocks: IMonthBlock[] = renderView;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-0 left-0 h-full w-max flex divide-x divide-custom-border-100/50">
|
<div className="absolute top-0 left-0 min-h-full h-max w-max flex divide-x divide-custom-border-100/50">
|
||||||
{monthBlocks?.map((block, rootIndex) => (
|
{monthBlocks?.map((block, rootIndex) => (
|
||||||
<div key={`month-${block?.month}-${block?.year}`} className="relative">
|
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
|
||||||
<div
|
<div
|
||||||
className="w-full sticky top-0 z-[5] bg-custom-background-100"
|
className="w-full sticky top-0 z-[5] bg-custom-background-100"
|
||||||
style={{
|
style={{
|
||||||
|
@ -92,7 +92,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
|||||||
id: inboxIssueId,
|
id: inboxIssueId,
|
||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
element: "Inbox page",
|
element: "Inbox page",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
router.push({
|
router.push({
|
||||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||||
@ -269,12 +269,17 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
|||||||
<DayPicker
|
<DayPicker
|
||||||
selected={date ? new Date(date) : undefined}
|
selected={date ? new Date(date) : undefined}
|
||||||
defaultMonth={date ? new Date(date) : undefined}
|
defaultMonth={date ? new Date(date) : undefined}
|
||||||
onSelect={(date) => { if (!date) return; setDate(date) }}
|
onSelect={(date) => {
|
||||||
|
if (!date) return;
|
||||||
|
setDate(date);
|
||||||
|
}}
|
||||||
mode="single"
|
mode="single"
|
||||||
className="border border-custom-border-200 rounded-md p-3"
|
className="border border-custom-border-200 rounded-md p-3"
|
||||||
disabled={[{
|
disabled={[
|
||||||
|
{
|
||||||
before: tomorrow,
|
before: tomorrow,
|
||||||
}]}
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
@ -54,7 +54,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
|
|||||||
showToast: boolean = true
|
showToast: boolean = true
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
|
await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Issue updated successfully",
|
title: "Issue updated successfully",
|
||||||
@ -64,7 +64,7 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
|
|||||||
}
|
}
|
||||||
captureIssueEvent({
|
captureIssueEvent({
|
||||||
eventName: "Inbox issue updated",
|
eventName: "Inbox issue updated",
|
||||||
payload: { ...response, state: "SUCCESS", element: "Inbox" },
|
payload: { ...data, state: "SUCCESS", element: "Inbox" },
|
||||||
updates: {
|
updates: {
|
||||||
changed_property: Object.keys(data).join(","),
|
changed_property: Object.keys(data).join(","),
|
||||||
change_details: Object.values(data).join(","),
|
change_details: Object.values(data).join(","),
|
||||||
|
@ -154,6 +154,10 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
disabled={!is_editable}
|
disabled={!is_editable}
|
||||||
|
isInboxIssue
|
||||||
|
onLabelUpdate={(val: string[]) =>
|
||||||
|
issueOperations.update(workspaceSlug, projectId, issueId, { label_ids: val })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,8 @@ import { TIssueComment } from "@plane/types";
|
|||||||
// icons
|
// icons
|
||||||
import { Globe2, Lock } from "lucide-react";
|
import { Globe2, Lock } from "lucide-react";
|
||||||
import { useMention, useWorkspace } from "hooks/store";
|
import { useMention, useWorkspace } from "hooks/store";
|
||||||
|
// helpers
|
||||||
|
import { isEmptyHtmlString } from "helpers/string.helper";
|
||||||
|
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
@ -51,10 +53,10 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
|
watch,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
reset,
|
reset,
|
||||||
watch,
|
} = useForm<Partial<TIssueComment>>({ defaultValues: { comment_html: "<p></p>" } });
|
||||||
} = useForm<Partial<TIssueComment>>({ defaultValues: { comment_html: "" } });
|
|
||||||
|
|
||||||
const onSubmit = async (formData: Partial<TIssueComment>) => {
|
const onSubmit = async (formData: Partial<TIssueComment>) => {
|
||||||
await activityOperations.createComment(formData).finally(() => {
|
await activityOperations.createComment(formData).finally(() => {
|
||||||
@ -63,14 +65,19 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isEmpty =
|
||||||
|
watch("comment_html") === "" ||
|
||||||
|
watch("comment_html")?.trim() === "" ||
|
||||||
|
watch("comment_html") === "<p></p>" ||
|
||||||
|
isEmptyHtmlString(watch("comment_html") ?? "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
// onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey && !isEmpty) {
|
||||||
// e.preventDefault();
|
handleSubmit(onSubmit)(e);
|
||||||
// // handleSubmit(onSubmit)(e);
|
}
|
||||||
// }
|
}}
|
||||||
// }}
|
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="access"
|
name="access"
|
||||||
@ -81,15 +88,12 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<LiteTextEditorWithRef
|
<LiteTextEditorWithRef
|
||||||
onEnterKeyPress={(e) => {
|
|
||||||
handleSubmit(onSubmit)(e);
|
|
||||||
}}
|
|
||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={value ?? ""}
|
value={!value ? "<p></p>" : value}
|
||||||
customClassName="p-2"
|
customClassName="p-2"
|
||||||
editorContentCustomClassNames="min-h-[35px]"
|
editorContentCustomClassNames="min-h-[35px]"
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
@ -105,7 +109,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||||||
}
|
}
|
||||||
submitButton={
|
submitButton={
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting || watch("comment_html") === ""}
|
disabled={isSubmitting || isEmpty}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="!px-2.5 !py-1.5 !text-xs"
|
className="!px-2.5 !py-1.5 !text-xs"
|
||||||
|
@ -13,6 +13,8 @@ export type TIssueLabel = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
isInboxIssue?: boolean;
|
||||||
|
onLabelUpdate?: (labelIds: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLabelOperations = {
|
export type TLabelOperations = {
|
||||||
@ -21,7 +23,7 @@ export type TLabelOperations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
const { workspaceSlug, projectId, issueId, disabled = false, isInboxIssue = false, onLabelUpdate } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { updateIssue } = useIssueDetail();
|
const { updateIssue } = useIssueDetail();
|
||||||
const { createLabel } = useLabel();
|
const { createLabel } = useLabel();
|
||||||
@ -31,7 +33,9 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
|||||||
() => ({
|
() => ({
|
||||||
updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
try {
|
try {
|
||||||
await updateIssue(workspaceSlug, projectId, issueId, data);
|
if (onLabelUpdate) onLabelUpdate(data.label_ids || []);
|
||||||
|
else await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
if (!isInboxIssue)
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Issue updated successfully",
|
title: "Issue updated successfully",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -48,6 +52,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
|||||||
createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
|
createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
|
||||||
try {
|
try {
|
||||||
const labelResponse = await createLabel(workspaceSlug, projectId, data);
|
const labelResponse = await createLabel(workspaceSlug, projectId, data);
|
||||||
|
if (!isInboxIssue)
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Label created successfully",
|
title: "Label created successfully",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -64,7 +69,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[updateIssue, createLabel, setToastAlert]
|
[updateIssue, createLabel, setToastAlert, onLabelUpdate]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -80,6 +80,13 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (query !== "" && e.key === "Escape") {
|
||||||
|
e.stopPropagation();
|
||||||
|
setQuery("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!issue) return <></>;
|
if (!issue) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -118,6 +125,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
onKeyDown={searchInputKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -360,7 +360,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="h-full w-full sm:w-1/2 md:w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5 fixed md:relative bg-custom-sidebar-background-100 right-0 z-[5]"
|
className="h-full w-full min-w-[300px] lg:min-w-80 xl:min-w-96 sm:w-1/2 md:w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5 fixed md:relative bg-custom-sidebar-background-100 right-0 z-[5]"
|
||||||
style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
|
style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
|
||||||
>
|
>
|
||||||
<IssueDetailsSidebar
|
<IssueDetailsSidebar
|
||||||
|
@ -133,7 +133,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-full w-full overflow-y-auto px-5">
|
<div className="h-full w-full overflow-y-auto px-6">
|
||||||
<h5 className="text-sm font-medium mt-6">Properties</h5>
|
<h5 className="text-sm font-medium mt-6">Properties</h5>
|
||||||
{/* TODO: render properties using a common component */}
|
{/* TODO: render properties using a common component */}
|
||||||
<div className={`mt-3 mb-2 space-y-2.5 ${!is_editable ? "opacity-60" : ""}`}>
|
<div className={`mt-3 mb-2 space-y-2.5 ${!is_editable ? "opacity-60" : ""}`}>
|
||||||
|
@ -67,7 +67,7 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
|||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<span>
|
<span>
|
||||||
<span className="hidden sm:block">Loading</span>...
|
<span className="hidden sm:block">Loading...</span>
|
||||||
</span>
|
</span>
|
||||||
) : isSubscribed ? (
|
) : isSubscribed ? (
|
||||||
<div className="hidden sm:block">Unsubscribe</div>
|
<div className="hidden sm:block">Unsubscribe</div>
|
||||||
|
@ -49,16 +49,17 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
||||||
|
|
||||||
const duplicateIssuePayload = omit(
|
const duplicateIssuePayload = omit(
|
||||||
{
|
{
|
||||||
...issue,
|
...issue,
|
||||||
name: `${issue.name} (copy)`,
|
name: `${issue.name} (copy)`,
|
||||||
|
is_draft: isDraftIssue ? false : issue.is_draft,
|
||||||
},
|
},
|
||||||
["id"]
|
["id"]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteIssueModal
|
<DeleteIssueModal
|
||||||
|
@ -60,7 +60,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
|||||||
<DraftKanBanLayout />
|
<DraftKanBanLayout />
|
||||||
) : null}
|
) : null}
|
||||||
{/* issue peek overview */}
|
{/* issue peek overview */}
|
||||||
<IssuePeekOverview />
|
<IssuePeekOverview is_draft />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
StateDropdown,
|
StateDropdown,
|
||||||
} from "components/dropdowns";
|
} from "components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
|
import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
@ -162,6 +162,10 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.description_html) setValue("description_html", data?.description_html);
|
||||||
|
}, [data?.description_html]);
|
||||||
|
|
||||||
const issueName = watch("name");
|
const issueName = watch("name");
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
|
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
|
||||||
@ -365,6 +369,28 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
{data?.description_html === undefined ? (
|
||||||
|
<Loader className="min-h-[7rem] space-y-2 py-2 border border-custom-border-200 rounded-md p-2 overflow-hidden">
|
||||||
|
<Loader.Item width="100%" height="26px" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader.Item width="26px" height="26px" />
|
||||||
|
<Loader.Item width="400px" height="26px" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader.Item width="26px" height="26px" />
|
||||||
|
<Loader.Item width="400px" height="26px" />
|
||||||
|
</div>
|
||||||
|
<Loader.Item width="80%" height="26px" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader.Item width="50%" height="26px" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
|
||||||
|
<Loader.Item width="100px" height="26px" />
|
||||||
|
<Loader.Item width="50px" height="26px" />
|
||||||
|
</div>
|
||||||
|
</Loader>
|
||||||
|
) : (
|
||||||
|
<Fragment>
|
||||||
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
|
<div className="absolute bottom-3.5 right-3.5 z-10 border-0.5 flex items-center gap-2">
|
||||||
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
|
{issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && (
|
||||||
<button
|
<button
|
||||||
@ -428,6 +454,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
? watch("description_html")
|
? watch("description_html")
|
||||||
: value
|
: value
|
||||||
}
|
}
|
||||||
|
initialValue={data?.description_html}
|
||||||
customClassName="min-h-[7rem] border-custom-border-100"
|
customClassName="min-h-[7rem] border-custom-border-100"
|
||||||
onChange={(description: Object, description_html: string) => {
|
onChange={(description: Object, description_html: string) => {
|
||||||
onChange(description_html);
|
onChange(description_html);
|
||||||
@ -439,6 +466,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Controller
|
<Controller
|
||||||
|
@ -3,7 +3,16 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useEventTracker, useCycle, useIssues, useModule, useProject, useWorkspace } from "hooks/store";
|
import {
|
||||||
|
useApplication,
|
||||||
|
useEventTracker,
|
||||||
|
useCycle,
|
||||||
|
useIssues,
|
||||||
|
useModule,
|
||||||
|
useProject,
|
||||||
|
useWorkspace,
|
||||||
|
useIssueDetail,
|
||||||
|
} from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
@ -39,6 +48,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
|
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
|
||||||
const [createMore, setCreateMore] = useState(false);
|
const [createMore, setCreateMore] = useState(false);
|
||||||
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
||||||
|
const [description, setDescription] = useState<string | undefined>(undefined);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureIssueEvent } = useEventTracker();
|
const { captureIssueEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
@ -53,7 +63,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE);
|
const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE);
|
||||||
const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||||
const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE);
|
const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE);
|
||||||
const { issues: draftIssueStore } = useIssues(EIssuesStoreType.DRAFT);
|
const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT);
|
||||||
|
const { fetchIssue } = useIssueDetail();
|
||||||
// store mapping based on current store
|
// store mapping based on current store
|
||||||
const issueStores = {
|
const issueStores = {
|
||||||
[EIssuesStoreType.PROJECT]: {
|
[EIssuesStoreType.PROJECT]: {
|
||||||
@ -86,7 +97,20 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
// current store details
|
// current store details
|
||||||
const { store: currentIssueStore, viewId } = issueStores[storeType];
|
const { store: currentIssueStore, viewId } = issueStores[storeType];
|
||||||
|
|
||||||
|
const fetchIssueDetail = async (issueId: string | undefined) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
if (issueId === undefined) {
|
||||||
|
setDescription("<p></p>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await fetchIssue(workspaceSlug, projectId, issueId, isDraft ? "DRAFT" : "DEFAULT");
|
||||||
|
if (response) setDescription(response?.description_html || "<p></p>");
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// fetching issue details
|
||||||
|
if (isOpen) fetchIssueDetail(data?.id);
|
||||||
|
|
||||||
// if modal is closed, reset active project to null
|
// if modal is closed, reset active project to null
|
||||||
// and return to avoid activeProjectId being set to some other project
|
// and return to avoid activeProjectId being set to some other project
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@ -105,6 +129,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
// in the url. This has the least priority.
|
// in the url. This has the least priority.
|
||||||
if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId)
|
if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId)
|
||||||
setActiveProjectId(projectId ?? workspaceProjectIds?.[0]);
|
setActiveProjectId(projectId ?? workspaceProjectIds?.[0]);
|
||||||
|
|
||||||
|
// clearing up the description state when we leave the component
|
||||||
|
return () => setDescription(undefined);
|
||||||
}, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]);
|
}, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]);
|
||||||
|
|
||||||
const addIssueToCycle = async (issue: TIssue, cycleId: string) => {
|
const addIssueToCycle = async (issue: TIssue, cycleId: string) => {
|
||||||
@ -142,7 +169,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = is_draft_issue
|
const response = is_draft_issue
|
||||||
? await draftIssueStore.createIssue(workspaceSlug, payload.project_id, payload)
|
? await draftIssues.createIssue(workspaceSlug, payload.project_id, payload)
|
||||||
: await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId);
|
: await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId);
|
||||||
if (!response) throw new Error();
|
if (!response) throw new Error();
|
||||||
|
|
||||||
@ -183,7 +210,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId);
|
isDraft
|
||||||
|
? await draftIssues.updateIssue(workspaceSlug, payload.project_id, data.id, payload)
|
||||||
|
: await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId);
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -261,6 +291,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
changesMade={changesMade}
|
changesMade={changesMade}
|
||||||
data={{
|
data={{
|
||||||
...data,
|
...data,
|
||||||
|
description_html: description,
|
||||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||||
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||||
}}
|
}}
|
||||||
@ -276,6 +307,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
<IssueFormRoot
|
<IssueFormRoot
|
||||||
data={{
|
data={{
|
||||||
...data,
|
...data,
|
||||||
|
description_html: description,
|
||||||
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null,
|
||||||
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null,
|
||||||
}}
|
}}
|
||||||
|
@ -15,6 +15,7 @@ import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
|
|||||||
|
|
||||||
interface IIssuePeekOverview {
|
interface IIssuePeekOverview {
|
||||||
is_archived?: boolean;
|
is_archived?: boolean;
|
||||||
|
is_draft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TIssuePeekOperations = {
|
export type TIssuePeekOperations = {
|
||||||
@ -45,7 +46,7 @@ export type TIssuePeekOperations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||||
const { is_archived = false } = props;
|
const { is_archived = false, is_draft = false } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
// router
|
// router
|
||||||
@ -72,7 +73,12 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
() => ({
|
() => ({
|
||||||
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
await fetchIssue(workspaceSlug, projectId, issueId, is_archived);
|
await fetchIssue(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueId,
|
||||||
|
is_archived ? "ARCHIVED" : is_draft ? "DRAFT" : "DEFAULT"
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching the parent issue");
|
console.error("Error fetching the parent issue");
|
||||||
}
|
}
|
||||||
@ -302,6 +308,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
is_archived,
|
is_archived,
|
||||||
|
is_draft,
|
||||||
fetchIssue,
|
fetchIssue,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
|
@ -62,6 +62,7 @@ export const UserDetails: React.FC<Props> = observer((props) => {
|
|||||||
formState: { errors, isSubmitting, isValid },
|
formState: { errors, isSubmitting, isValid },
|
||||||
} = useForm<IUser>({
|
} = useForm<IUser>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (formData: IUser) => {
|
const onSubmit = async (formData: IUser) => {
|
||||||
@ -164,15 +165,16 @@ export const UserDetails: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
<div className="my-2 mr-10 flex w-full rounded-md bg-onboarding-background-200 text-sm">
|
<div className="my-2 mr-10 flex w-full rounded-md bg-onboarding-background-200 text-sm">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="first_name"
|
name="first_name"
|
||||||
rules={{
|
rules={{
|
||||||
required: "First name is required",
|
required: "Name is required",
|
||||||
maxLength: {
|
maxLength: {
|
||||||
value: 24,
|
value: 24,
|
||||||
message: "First name cannot exceed the limit of 24 characters",
|
message: "Name must be within 24 characters.",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
@ -190,11 +192,12 @@ export const UserDetails: React.FC<Props> = observer((props) => {
|
|||||||
hasError={Boolean(errors.first_name)}
|
hasError={Boolean(errors.first_name)}
|
||||||
placeholder="Enter your full name..."
|
placeholder="Enter your full name..."
|
||||||
className="w-full border-onboarding-border-100 focus:border-custom-primary-100"
|
className="w-full border-onboarding-border-100 focus:border-custom-primary-100"
|
||||||
maxLength={24}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{errors.first_name && <span className="text-sm text-red-500">{errors.first_name.message}</span>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-10 mt-14">
|
<div className="mb-10 mt-14">
|
||||||
<Controller
|
<Controller
|
||||||
|
@ -37,6 +37,7 @@ type Props = {
|
|||||||
snapshot?: DraggableStateSnapshot;
|
snapshot?: DraggableStateSnapshot;
|
||||||
handleCopyText: () => void;
|
handleCopyText: () => void;
|
||||||
shortContextMenu?: boolean;
|
shortContextMenu?: boolean;
|
||||||
|
disableDrag?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigation = (workspaceSlug: string, projectId: string) => [
|
const navigation = (workspaceSlug: string, projectId: string) => [
|
||||||
@ -79,7 +80,7 @@ const navigation = (workspaceSlug: string, projectId: string) => [
|
|||||||
|
|
||||||
export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { projectId, provided, snapshot, handleCopyText, shortContextMenu = false } = props;
|
const { projectId, provided, snapshot, handleCopyText, shortContextMenu = false, disableDrag } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { theme: themeStore } = useApplication();
|
const { theme: themeStore } = useApplication();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
@ -163,7 +164,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
snapshot?.isDragging ? "opacity-60" : ""
|
snapshot?.isDragging ? "opacity-60" : ""
|
||||||
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
||||||
>
|
>
|
||||||
{provided && (
|
{provided && !disableDrag && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"}
|
tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"}
|
||||||
position="top-right"
|
position="top-right"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, FC, useRef, useEffect } from "react";
|
import { useState, FC, useRef, useEffect, useCallback } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
|
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
@ -11,9 +11,11 @@ import useToast from "hooks/use-toast";
|
|||||||
import { CreateProjectModal, ProjectSidebarListItem } from "components/project";
|
import { CreateProjectModal, ProjectSidebarListItem } from "components/project";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
|
import { orderJoinedProjects } from "helpers/project.helper";
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
import { IProject } from "@plane/types";
|
||||||
|
|
||||||
export const ProjectSidebarList: FC = observer(() => {
|
export const ProjectSidebarList: FC = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -32,9 +34,9 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const {
|
const {
|
||||||
|
getProjectById,
|
||||||
joinedProjectIds: joinedProjects,
|
joinedProjectIds: joinedProjects,
|
||||||
favoriteProjectIds: favoriteProjects,
|
favoriteProjectIds: favoriteProjects,
|
||||||
orderProjectsWithSortOrder,
|
|
||||||
updateProjectView,
|
updateProjectView,
|
||||||
} = useProject();
|
} = useProject();
|
||||||
// router
|
// router
|
||||||
@ -55,15 +57,20 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDragEnd = async (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
const { source, destination, draggableId } = result;
|
const { source, destination, draggableId } = result;
|
||||||
|
|
||||||
if (!destination || !workspaceSlug) return;
|
if (!destination || !workspaceSlug) return;
|
||||||
|
|
||||||
if (source.index === destination.index) return;
|
if (source.index === destination.index) return;
|
||||||
|
|
||||||
const updatedSortOrder = orderProjectsWithSortOrder(source.index, destination.index, draggableId);
|
const joinedProjectsList: IProject[] = [];
|
||||||
|
joinedProjects.map((projectId) => {
|
||||||
|
const _project = getProjectById(projectId);
|
||||||
|
if (_project) joinedProjectsList.push(_project);
|
||||||
|
});
|
||||||
|
if (joinedProjectsList.length <= 0) return;
|
||||||
|
|
||||||
|
const updatedSortOrder = orderJoinedProjects(source.index, destination.index, draggableId, joinedProjectsList);
|
||||||
|
if (updatedSortOrder != undefined)
|
||||||
updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => {
|
updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -176,6 +183,7 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
handleCopyText={() => handleCopyText(projectId)}
|
handleCopyText={() => handleCopyText(projectId)}
|
||||||
shortContextMenu
|
shortContextMenu
|
||||||
|
disableDrag
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
45
web/helpers/project.helper.ts
Normal file
45
web/helpers/project.helper.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { IProject } from "@plane/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the sort order of the project.
|
||||||
|
* @param sortIndex
|
||||||
|
* @param destinationIndex
|
||||||
|
* @param projectId
|
||||||
|
* @returns number | undefined
|
||||||
|
*/
|
||||||
|
export const orderJoinedProjects = (
|
||||||
|
sourceIndex: number,
|
||||||
|
destinationIndex: number,
|
||||||
|
currentProjectId: string,
|
||||||
|
joinedProjects: IProject[]
|
||||||
|
): number | undefined => {
|
||||||
|
if (!currentProjectId || sourceIndex < 0 || destinationIndex < 0 || joinedProjects.length <= 0) return undefined;
|
||||||
|
|
||||||
|
let updatedSortOrder: number | undefined = undefined;
|
||||||
|
const sortOrderDefaultValue = 10000;
|
||||||
|
|
||||||
|
if (destinationIndex === 0) {
|
||||||
|
// updating project at the top of the project
|
||||||
|
const currentSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||||
|
updatedSortOrder = currentSortOrder - sortOrderDefaultValue;
|
||||||
|
} else if (destinationIndex === joinedProjects.length - 1) {
|
||||||
|
// updating project at the bottom of the project
|
||||||
|
const currentSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0;
|
||||||
|
updatedSortOrder = currentSortOrder + sortOrderDefaultValue;
|
||||||
|
} else {
|
||||||
|
// updating project in the middle of the project
|
||||||
|
if (sourceIndex > destinationIndex) {
|
||||||
|
const destinationTopProjectSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0;
|
||||||
|
const destinationBottomProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||||
|
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
|
||||||
|
updatedSortOrder = updatedValue;
|
||||||
|
} else {
|
||||||
|
const destinationTopProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||||
|
const destinationBottomProjectSortOrder = joinedProjects[destinationIndex + 1].sort_order || 0;
|
||||||
|
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
|
||||||
|
updatedSortOrder = updatedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedSortOrder;
|
||||||
|
};
|
@ -4,6 +4,7 @@ import {
|
|||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
VIEW_ISSUES,
|
VIEW_ISSUES,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
import * as DOMPurify from 'dompurify';
|
||||||
|
|
||||||
export const addSpaceIfCamelCase = (str: string) => {
|
export const addSpaceIfCamelCase = (str: string) => {
|
||||||
if (str === undefined || str === null) return "";
|
if (str === undefined || str === null) return "";
|
||||||
@ -224,3 +225,10 @@ export const checkEmailValidity = (email: string): boolean => {
|
|||||||
|
|
||||||
return isEmailValid;
|
return isEmailValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isEmptyHtmlString = (htmlString: string) => {
|
||||||
|
// Remove HTML tags using regex
|
||||||
|
const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] });
|
||||||
|
// Trim the string and check if it's empty
|
||||||
|
return cleanText.trim() === "";
|
||||||
|
};
|
||||||
|
@ -28,8 +28,9 @@ export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
|
|||||||
const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed");
|
const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full realtive flex flex-row">
|
<div className="h-full w-full md:flex md:flex-row-reverse md:overflow-hidden">
|
||||||
<div className="w-full realtive flex flex-col">
|
<ProfileSidebar />
|
||||||
|
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
|
||||||
<ProfileNavbar isAuthorized={isAuthorized} showProfileIssuesFilter={showProfileIssuesFilter} />
|
<ProfileNavbar isAuthorized={isAuthorized} showProfileIssuesFilter={showProfileIssuesFilter} />
|
||||||
{isAuthorized || !isAuthorizedPath ? (
|
{isAuthorized || !isAuthorizedPath ? (
|
||||||
<div className={`w-full overflow-hidden md:h-full ${className}`}>{children}</div>
|
<div className={`w-full overflow-hidden md:h-full ${className}`}>{children}</div>
|
||||||
@ -39,8 +40,6 @@ export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProfileSidebar />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -50,7 +50,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
|||||||
<CrispWrapper user={currentUser}>
|
<CrispWrapper user={currentUser}>
|
||||||
<PostHogProvider
|
<PostHogProvider
|
||||||
user={currentUser}
|
user={currentUser}
|
||||||
currentWorkspaceId= {currentWorkspace?.id}
|
currentWorkspaceId={currentWorkspace?.id}
|
||||||
workspaceRole={currentWorkspaceRole}
|
workspaceRole={currentWorkspaceRole}
|
||||||
projectRole={currentProjectRole}
|
projectRole={currentProjectRole}
|
||||||
posthogAPIKey={envConfig?.posthog_api_key || null}
|
posthogAPIKey={envConfig?.posthog_api_key || null}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo run develop",
|
"dev": "turbo run develop",
|
||||||
@ -34,6 +34,7 @@
|
|||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"dompurify": "^3.0.9",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@ -61,6 +62,7 @@
|
|||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/js-cookie": "^3.0.2",
|
"@types/js-cookie": "^3.0.2",
|
||||||
"@types/lodash": "^4.14.202",
|
"@types/lodash": "^4.14.202",
|
||||||
"@types/node": "18.0.6",
|
"@types/node": "18.0.6",
|
||||||
|
@ -42,7 +42,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}`
|
? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}`
|
||||||
: null,
|
: null,
|
||||||
workspaceSlug && projectId && archivedIssueId
|
workspaceSlug && projectId && archivedIssueId
|
||||||
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), true)
|
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), "ARCHIVED")
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { useProject, useInboxIssues } from "hooks/store";
|
|||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
|
import { InboxLayoutLoader } from "components/ui";
|
||||||
import { PageHead } from "components/core";
|
import { PageHead } from "components/core";
|
||||||
import { ProjectInboxHeader } from "components/headers";
|
import { ProjectInboxHeader } from "components/headers";
|
||||||
import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
|
import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
|
||||||
@ -23,7 +24,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
|||||||
issues: { fetchInboxIssues },
|
issues: { fetchInboxIssues },
|
||||||
} = useInboxIssues();
|
} = useInboxIssues();
|
||||||
// fetching the Inbox filters and issues
|
// fetching the Inbox filters and issues
|
||||||
useSWR(
|
const { isLoading } = useSWR(
|
||||||
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
|
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
|
||||||
? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}`
|
? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}`
|
||||||
: null,
|
: null,
|
||||||
@ -37,7 +38,12 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
|||||||
// derived values
|
// derived values
|
||||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : undefined;
|
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : undefined;
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>;
|
if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view || isLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<InboxLayoutLoader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -42,8 +42,10 @@ export class IssueDraftService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string, queries?: any): Promise<any> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`, {
|
||||||
|
params: queries,
|
||||||
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response;
|
throw error?.response;
|
||||||
|
@ -72,9 +72,6 @@ export class ProjectService extends APIService {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: {
|
data: {
|
||||||
view_props?: IProjectViewProps;
|
|
||||||
default_props?: IProjectViewProps;
|
|
||||||
preferences?: ProjectPreferences;
|
|
||||||
sort_order?: number;
|
sort_order?: number;
|
||||||
}
|
}
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
import { isFuture, isPast } from "date-fns";
|
import { isFuture, isPast, isToday } from "date-fns";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import sortBy from "lodash/sortBy";
|
import sortBy from "lodash/sortBy";
|
||||||
// types
|
// types
|
||||||
@ -118,7 +118,8 @@ export class CycleStore implements ICycleStore {
|
|||||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||||
let completedCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
|
let completedCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
|
||||||
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
|
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
|
||||||
return c.project_id === projectId && hasEndDatePassed;
|
const isEndDateToday = isToday(new Date(c.end_date ?? ""));
|
||||||
|
return c.project_id === projectId && hasEndDatePassed && !isEndDateToday;
|
||||||
});
|
});
|
||||||
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
|
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
|
||||||
const completedCycleIds = completedCycles.map((c) => c.id);
|
const completedCycleIds = completedCycles.map((c) => c.id);
|
||||||
|
@ -53,7 +53,7 @@ export interface IInboxIssue {
|
|||||||
inboxId: string,
|
inboxId: string,
|
||||||
inboxIssueId: string,
|
inboxIssueId: string,
|
||||||
data: Partial<TInboxIssueExtendedDetail>
|
data: Partial<TInboxIssueExtendedDetail>
|
||||||
) => Promise<TInboxIssueExtendedDetail>;
|
) => Promise<void>;
|
||||||
removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
|
removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
|
||||||
updateInboxIssueStatus: (
|
updateInboxIssueStatus: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
@ -61,7 +61,7 @@ export interface IInboxIssue {
|
|||||||
inboxId: string,
|
inboxId: string,
|
||||||
inboxIssueId: string,
|
inboxIssueId: string,
|
||||||
data: TInboxDetailedStatus
|
data: TInboxDetailedStatus
|
||||||
) => Promise<TInboxIssueExtendedDetail>;
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InboxIssue implements IInboxIssue {
|
export class InboxIssue implements IInboxIssue {
|
||||||
@ -215,22 +215,9 @@ export class InboxIssue implements IInboxIssue {
|
|||||||
issue: data,
|
issue: data,
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
this.rootStore.inbox.rootStore.issue.issues.updateIssue(inboxIssueId, data);
|
||||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
|
||||||
this.rootStore.inbox.rootStore.issue.issues.updateIssue(issue.id, issue);
|
|
||||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
|
||||||
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
|
|
||||||
});
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
|
|
||||||
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
|
|
||||||
return uniq(concat(inboxIssueIds, response.id));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||||
return response as any;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -238,7 +225,7 @@ export class InboxIssue implements IInboxIssue {
|
|||||||
|
|
||||||
removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
|
removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
pull(this.inboxIssues[inboxId], inboxIssueId);
|
pull(this.inboxIssues[inboxId], inboxIssueId);
|
||||||
@ -248,7 +235,6 @@ export class InboxIssue implements IInboxIssue {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||||
return response as any;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -262,34 +248,18 @@ export class InboxIssue implements IInboxIssue {
|
|||||||
data: TInboxDetailedStatus
|
data: TInboxDetailedStatus
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.inboxIssueService.updateInboxIssueStatus(
|
await this.inboxIssueService.updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data);
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
inboxId,
|
|
||||||
inboxIssueId,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
|
|
||||||
const pendingStatus = -2;
|
const pendingStatus = -2;
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
set(this.inboxIssueMap, [inboxId, inboxIssueId, "status"], data.status);
|
||||||
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
|
|
||||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
|
||||||
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
|
|
||||||
update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) =>
|
update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) =>
|
||||||
data.status === pendingStatus ? count + 1 : count - 1
|
data.status === pendingStatus ? count + 1 : count - 1
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
|
|
||||||
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
|
|
||||||
return uniq(concat(inboxIssueIds, response.id));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||||
return response as any;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,9 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
|
|||||||
|
|
||||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
try {
|
try {
|
||||||
await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
|
await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
|
||||||
|
this.rootStore.issues.updateIssue(issueId, data);
|
||||||
|
|
||||||
if (data.hasOwnProperty("is_draft") && data?.is_draft === false) {
|
if (data.hasOwnProperty("is_draft") && data?.is_draft === false) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { makeObservable } from "mobx";
|
import { makeObservable } from "mobx";
|
||||||
// services
|
// services
|
||||||
import { IssueArchiveService, IssueService } from "services/issue";
|
import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
@ -8,7 +8,12 @@ import { IIssueDetail } from "./root.store";
|
|||||||
|
|
||||||
export interface IIssueStoreActions {
|
export interface IIssueStoreActions {
|
||||||
// actions
|
// actions
|
||||||
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise<TIssue>;
|
fetchIssue: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
issueType?: "DEFAULT" | "DRAFT" | "ARCHIVED"
|
||||||
|
) => Promise<TIssue>;
|
||||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||||
@ -34,6 +39,7 @@ export class IssueStore implements IIssueStore {
|
|||||||
// services
|
// services
|
||||||
issueService;
|
issueService;
|
||||||
issueArchiveService;
|
issueArchiveService;
|
||||||
|
issueDraftService;
|
||||||
|
|
||||||
constructor(rootStore: IIssueDetail) {
|
constructor(rootStore: IIssueDetail) {
|
||||||
makeObservable(this, {});
|
makeObservable(this, {});
|
||||||
@ -42,6 +48,7 @@ export class IssueStore implements IIssueStore {
|
|||||||
// services
|
// services
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
this.issueArchiveService = new IssueArchiveService();
|
this.issueArchiveService = new IssueArchiveService();
|
||||||
|
this.issueDraftService = new IssueDraftService();
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper methods
|
// helper methods
|
||||||
@ -51,21 +58,54 @@ export class IssueStore implements IIssueStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => {
|
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, issueType = "DEFAULT") => {
|
||||||
try {
|
try {
|
||||||
const query = {
|
const query = {
|
||||||
expand: "issue_reactions,issue_attachment,issue_link,parent",
|
expand: "issue_reactions,issue_attachment,issue_link,parent",
|
||||||
};
|
};
|
||||||
|
|
||||||
let issue: TIssue;
|
let issue: TIssue;
|
||||||
|
let issuePayload: TIssue;
|
||||||
|
|
||||||
if (isArchived)
|
if (issueType === "ARCHIVED")
|
||||||
issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query);
|
issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query);
|
||||||
|
else if (issueType === "DRAFT")
|
||||||
|
issue = await this.issueDraftService.getDraftIssueById(workspaceSlug, projectId, issueId, query);
|
||||||
else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
|
else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
|
||||||
|
|
||||||
if (!issue) throw new Error("Issue not found");
|
if (!issue) throw new Error("Issue not found");
|
||||||
|
|
||||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue], true);
|
issuePayload = {
|
||||||
|
id: issue?.id,
|
||||||
|
sequence_id: issue?.sequence_id,
|
||||||
|
name: issue?.name,
|
||||||
|
description_html: issue?.description_html,
|
||||||
|
sort_order: issue?.sort_order,
|
||||||
|
state_id: issue?.state_id,
|
||||||
|
priority: issue?.priority,
|
||||||
|
label_ids: issue?.label_ids,
|
||||||
|
assignee_ids: issue?.assignee_ids,
|
||||||
|
estimate_point: issue?.estimate_point,
|
||||||
|
sub_issues_count: issue?.sub_issues_count,
|
||||||
|
attachment_count: issue?.attachment_count,
|
||||||
|
link_count: issue?.link_count,
|
||||||
|
project_id: issue?.project_id,
|
||||||
|
parent_id: issue?.parent_id,
|
||||||
|
cycle_id: issue?.cycle_id,
|
||||||
|
module_ids: issue?.module_ids,
|
||||||
|
created_at: issue?.created_at,
|
||||||
|
updated_at: issue?.updated_at,
|
||||||
|
start_date: issue?.start_date,
|
||||||
|
target_date: issue?.target_date,
|
||||||
|
completed_at: issue?.completed_at,
|
||||||
|
archived_at: issue?.archived_at,
|
||||||
|
created_by: issue?.created_by,
|
||||||
|
updated_by: issue?.updated_by,
|
||||||
|
is_draft: issue?.is_draft,
|
||||||
|
is_subscribed: issue?.is_subscribed,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload], true);
|
||||||
|
|
||||||
// store handlers from issue detail
|
// store handlers from issue detail
|
||||||
// parent
|
// parent
|
||||||
|
@ -140,8 +140,12 @@ export class IssueDetail implements IIssueDetail {
|
|||||||
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
|
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
|
||||||
|
|
||||||
// issue
|
// issue
|
||||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) =>
|
fetchIssue = async (
|
||||||
this.issue.fetchIssue(workspaceSlug, projectId, issueId, isArchived);
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
issueType: "DEFAULT" | "ARCHIVED" | "DRAFT" = "DEFAULT"
|
||||||
|
) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueType);
|
||||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
||||||
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
|
import sortBy from "lodash/sortBy";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "../root.store";
|
import { RootStore } from "../root.store";
|
||||||
import { IProject } from "@plane/types";
|
import { IProject } from "@plane/types";
|
||||||
// services
|
// services
|
||||||
import { IssueLabelService, IssueService } from "services/issue";
|
import { IssueLabelService, IssueService } from "services/issue";
|
||||||
import { ProjectService, ProjectStateService } from "services/project";
|
import { ProjectService, ProjectStateService } from "services/project";
|
||||||
|
import { cloneDeep, update } from "lodash";
|
||||||
export interface IProjectStore {
|
export interface IProjectStore {
|
||||||
// observables
|
// observables
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@ -28,8 +30,6 @@ export interface IProjectStore {
|
|||||||
// favorites actions
|
// favorites actions
|
||||||
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
// project-order action
|
|
||||||
orderProjectsWithSortOrder: (sourceIndex: number, destinationIndex: number, projectId: string) => number;
|
|
||||||
// project-view action
|
// project-view action
|
||||||
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
|
updateProjectView: (workspaceSlug: string, projectId: string, viewProps: any) => Promise<any>;
|
||||||
// CRUD actions
|
// CRUD actions
|
||||||
@ -71,8 +71,6 @@ export class ProjectStore implements IProjectStore {
|
|||||||
// favorites actions
|
// favorites actions
|
||||||
addProjectToFavorites: action,
|
addProjectToFavorites: action,
|
||||||
removeProjectFromFavorites: action,
|
removeProjectFromFavorites: action,
|
||||||
// project-order action
|
|
||||||
orderProjectsWithSortOrder: action,
|
|
||||||
// project-view action
|
// project-view action
|
||||||
updateProjectView: action,
|
updateProjectView: action,
|
||||||
// CRUD actions
|
// CRUD actions
|
||||||
@ -128,7 +126,11 @@ export class ProjectStore implements IProjectStore {
|
|||||||
get joinedProjectIds() {
|
get joinedProjectIds() {
|
||||||
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
|
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
|
||||||
if (!currentWorkspace) return [];
|
if (!currentWorkspace) return [];
|
||||||
const projectIds = Object.values(this.projectMap ?? {})
|
|
||||||
|
let projects = Object.values(this.projectMap ?? {});
|
||||||
|
projects = sortBy(projects, "sort_order");
|
||||||
|
|
||||||
|
const projectIds = projects
|
||||||
.filter((project) => project.workspace === currentWorkspace.id && project.is_member)
|
.filter((project) => project.workspace === currentWorkspace.id && project.is_member)
|
||||||
.map((project) => project.id);
|
.map((project) => project.id);
|
||||||
return projectIds;
|
return projectIds;
|
||||||
@ -140,7 +142,11 @@ export class ProjectStore implements IProjectStore {
|
|||||||
get favoriteProjectIds() {
|
get favoriteProjectIds() {
|
||||||
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
|
const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace;
|
||||||
if (!currentWorkspace) return [];
|
if (!currentWorkspace) return [];
|
||||||
const projectIds = Object.values(this.projectMap ?? {})
|
|
||||||
|
let projects = Object.values(this.projectMap ?? {});
|
||||||
|
projects = sortBy(projects, "created_at");
|
||||||
|
|
||||||
|
const projectIds = projects
|
||||||
.filter((project) => project.workspace === currentWorkspace.id && project.is_favorite)
|
.filter((project) => project.workspace === currentWorkspace.id && project.is_favorite)
|
||||||
.map((project) => project.id);
|
.map((project) => project.id);
|
||||||
return projectIds;
|
return projectIds;
|
||||||
@ -253,41 +259,6 @@ export class ProjectStore implements IProjectStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the sort order of the project.
|
|
||||||
* @param sortIndex
|
|
||||||
* @param destinationIndex
|
|
||||||
* @param projectId
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
orderProjectsWithSortOrder = (sortIndex: number, destinationIndex: number, projectId: string) => {
|
|
||||||
try {
|
|
||||||
const workspaceSlug = this.rootStore.app.router.workspaceSlug;
|
|
||||||
if (!workspaceSlug) return 0;
|
|
||||||
const projectsList = Object.values(this.projectMap || {}) || [];
|
|
||||||
let updatedSortOrder = projectsList[sortIndex].sort_order;
|
|
||||||
if (destinationIndex === 0) updatedSortOrder = (projectsList[0].sort_order as number) - 1000;
|
|
||||||
else if (destinationIndex === projectsList.length - 1)
|
|
||||||
updatedSortOrder = (projectsList[projectsList.length - 1].sort_order as number) + 1000;
|
|
||||||
else {
|
|
||||||
const destinationSortingOrder = projectsList[destinationIndex].sort_order as number;
|
|
||||||
const relativeDestinationSortingOrder =
|
|
||||||
sortIndex < destinationIndex
|
|
||||||
? (projectsList[destinationIndex + 1].sort_order as number)
|
|
||||||
: (projectsList[destinationIndex - 1].sort_order as number);
|
|
||||||
|
|
||||||
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
|
|
||||||
}
|
|
||||||
runInAction(() => {
|
|
||||||
set(this.projectMap, [projectId, "sort_order"], updatedSortOrder);
|
|
||||||
});
|
|
||||||
return updatedSortOrder;
|
|
||||||
} catch (error) {
|
|
||||||
console.log("failed to update sort order of the projects");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the project view
|
* Updates the project view
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
@ -295,12 +266,18 @@ export class ProjectStore implements IProjectStore {
|
|||||||
* @param viewProps
|
* @param viewProps
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
updateProjectView = async (workspaceSlug: string, projectId: string, viewProps: any) => {
|
updateProjectView = async (workspaceSlug: string, projectId: string, viewProps: { sort_order: number }) => {
|
||||||
|
const currentProjectSortOrder = this.getProjectById(projectId)?.sort_order;
|
||||||
try {
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
set(this.projectMap, [projectId, "sort_order"], viewProps?.sort_order);
|
||||||
|
});
|
||||||
const response = await this.projectService.setProjectView(workspaceSlug, projectId, viewProps);
|
const response = await this.projectService.setProjectView(workspaceSlug, projectId, viewProps);
|
||||||
await this.fetchProjects(workspaceSlug);
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
set(this.projectMap, [projectId, "sort_order"], currentProjectSortOrder);
|
||||||
|
});
|
||||||
console.log("Failed to update sort order of the projects");
|
console.log("Failed to update sort order of the projects");
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
14
yarn.lock
14
yarn.lock
@ -2641,6 +2641,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.4.tgz#427a4ce8590727aed5ce0fe39a64f175a57fdc1c"
|
resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.4.tgz#427a4ce8590727aed5ce0fe39a64f175a57fdc1c"
|
||||||
integrity sha512-PD+wqNhrjWFjAlSVd18jvChZvOXB2SOwAILBmuYev5zswBats5qmzs/QFoooLKd2omj9BT05a8MeSeRmXLGY+Q==
|
integrity sha512-PD+wqNhrjWFjAlSVd18jvChZvOXB2SOwAILBmuYev5zswBats5qmzs/QFoooLKd2omj9BT05a8MeSeRmXLGY+Q==
|
||||||
|
|
||||||
|
"@types/dompurify@^3.0.5":
|
||||||
|
version "3.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7"
|
||||||
|
integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==
|
||||||
|
dependencies:
|
||||||
|
"@types/trusted-types" "*"
|
||||||
|
|
||||||
"@types/estree@*", "@types/estree@^1.0.0":
|
"@types/estree@*", "@types/estree@^1.0.0":
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||||
@ -2843,7 +2850,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776"
|
resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776"
|
||||||
integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==
|
integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==
|
||||||
|
|
||||||
"@types/trusted-types@^2.0.2":
|
"@types/trusted-types@*", "@types/trusted-types@^2.0.2":
|
||||||
version "2.0.7"
|
version "2.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||||
@ -4082,6 +4089,11 @@ dom4@^2.1.5:
|
|||||||
resolved "https://registry.yarnpkg.com/dom4/-/dom4-2.1.6.tgz#c90df07134aa0dbd81ed4d6ba1237b36fc164770"
|
resolved "https://registry.yarnpkg.com/dom4/-/dom4-2.1.6.tgz#c90df07134aa0dbd81ed4d6ba1237b36fc164770"
|
||||||
integrity sha512-JkCVGnN4ofKGbjf5Uvc8mmxaATIErKQKSgACdBXpsQ3fY6DlIpAyWfiBSrGkttATssbDCp3psiAKWXk5gmjycA==
|
integrity sha512-JkCVGnN4ofKGbjf5Uvc8mmxaATIErKQKSgACdBXpsQ3fY6DlIpAyWfiBSrGkttATssbDCp3psiAKWXk5gmjycA==
|
||||||
|
|
||||||
|
dompurify@^3.0.9:
|
||||||
|
version "3.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.9.tgz#b3f362f24b99f53498c75d43ecbd784b0b3ad65e"
|
||||||
|
integrity sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==
|
||||||
|
|
||||||
dot-case@^3.0.4:
|
dot-case@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
|
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
|
||||||
|
Loading…
Reference in New Issue
Block a user