Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28c9245fe1 | ||
|
|
d5cbe3283b | ||
|
|
ae931f8172 | ||
|
|
a8c6483c60 | ||
|
|
9c761a614f | ||
|
|
adf88a0f13 | ||
|
|
5d2983d027 | ||
|
|
8339daa3ee | ||
|
|
4a9e09a54a | ||
|
|
2c609670c8 | ||
|
|
dfcba4dfc1 | ||
|
|
d0e68cdcfb | ||
|
|
43103a1445 | ||
|
|
1c155f6cbe | ||
|
|
1707f4f282 | ||
|
|
c2c2ad0d7a | ||
|
|
1bf8f82ccb | ||
|
|
3bdd91e577 | ||
|
|
1f9c7a4b67 | ||
|
|
d1828c9496 | ||
|
|
3f87d8b99d | ||
|
|
aba6e603a3 | ||
|
|
b4f2176ffa |
@@ -34,6 +34,7 @@ from plane.db.models import (
|
||||
Project,
|
||||
IssueAttachment,
|
||||
IssueLink,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
|
||||
@@ -363,14 +364,28 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if cycle.owned_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the cycle"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk")
|
||||
).values_list("issue", flat=True)
|
||||
)
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
|
||||
@@ -390,29 +390,26 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
inbox_id=inbox.id,
|
||||
)
|
||||
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Check the inbox issue created
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
return Response(
|
||||
{"error": "You cannot delete inbox issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check the issue status
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
Issue.objects.filter(
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
).delete()
|
||||
).first()
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue.delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -310,11 +310,14 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
|
||||
serializer.save()
|
||||
# Refetch the issue
|
||||
issue = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]).first()
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
pk=serializer.data["id"],
|
||||
).first()
|
||||
issue.created_at = request.data.get("created_at")
|
||||
issue.save(update_fields=["created_at"])
|
||||
|
||||
|
||||
# Track the issue
|
||||
issue_activity.delay(
|
||||
type="issue.activity.created",
|
||||
@@ -386,6 +389,19 @@ class IssueAPIEndpoint(BaseAPIView):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ from plane.db.models import (
|
||||
ModuleIssue,
|
||||
ModuleLink,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
|
||||
from .base import BaseAPIView
|
||||
@@ -265,6 +266,20 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if module.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the module"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list(
|
||||
"issue", flat=True
|
||||
|
||||
@@ -47,6 +47,7 @@ from plane.db.models import (
|
||||
Label,
|
||||
User,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
|
||||
@@ -1039,14 +1040,28 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if cycle.owned_by_id != request.user.id and not (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or owner can delete the cycle"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk")
|
||||
).values_list("issue", flat=True)
|
||||
)
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
|
||||
@@ -553,28 +553,27 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
project_id=project_id,
|
||||
inbox_id=inbox_id,
|
||||
)
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
return Response(
|
||||
{"error": "You cannot delete inbox issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check the issue status
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
Issue.objects.filter(
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
).delete()
|
||||
).first()
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue.delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -44,6 +44,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -549,6 +550,20 @@ class IssueViewSet(BaseViewSet):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue.delete()
|
||||
issue_activity.delay(
|
||||
type="issue.activity.deleted",
|
||||
@@ -602,6 +617,19 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def delete(self, request, slug, project_id):
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists():
|
||||
|
||||
return Response(
|
||||
{"error": "Only admin can perform this action"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue_ids = request.data.get("issue_ids", [])
|
||||
|
||||
if not len(issue_ids):
|
||||
|
||||
@@ -40,6 +40,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
IssueSubscriber,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -380,6 +381,19 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the issue"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
issue.delete()
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.deleted",
|
||||
|
||||
@@ -48,6 +48,7 @@ from plane.db.models import (
|
||||
ModuleLink,
|
||||
ModuleUserProperties,
|
||||
Project,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
@@ -737,6 +738,21 @@ class ModuleViewSet(BaseViewSet):
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
if module.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or creator can delete the module"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list(
|
||||
"issue", flat=True
|
||||
|
||||
@@ -333,6 +333,20 @@ class PageViewSet(BaseViewSet):
|
||||
pk=pk, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
|
||||
if not page.owned_by_id != request.user.id and not (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only admin or owner can delete the page"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# only the owner and admin can delete the page
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
|
||||
@@ -116,6 +116,20 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||
pk=pk,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
if not (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists()
|
||||
and workspace_view.owned_by_id != request.user.id
|
||||
):
|
||||
return Response(
|
||||
{"error": "You do not have permission to delete this view"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
workspace_member = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
@@ -412,14 +426,16 @@ class IssueViewViewSet(BaseViewSet):
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
project_member = ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=20,
|
||||
is_active=True,
|
||||
)
|
||||
if project_member.exists() or project_view.owned_by == request.user:
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists()
|
||||
or project_view.owned_by_id == request.user.id
|
||||
):
|
||||
project_view.delete()
|
||||
else:
|
||||
return Response(
|
||||
|
||||
@@ -10,7 +10,7 @@ Let's get started!
|
||||
|
||||
<details>
|
||||
<summary>Option 1 - Using Cloud Server</summary>
|
||||
<p>Best way to start is to create EC2 maching on AWS. It must of minimum t3.medium/t3a/medium</p>
|
||||
<p>Best way to start is to create EC2 machine on AWS. It must have minimum of 2vCPU and 4GB RAM.</p>
|
||||
<p>Run the below command to install docker engine.</p>
|
||||
|
||||
`curl -fsSL https://get.docker.com | sh -`
|
||||
@@ -67,23 +67,6 @@ curl -fsSL -o setup.sh https://raw.githubusercontent.com/makeplane/plane/master/
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Downloading Preview Release</summary>
|
||||
|
||||
```
|
||||
mkdir plane-selfhost
|
||||
|
||||
cd plane-selfhost
|
||||
|
||||
export RELEASE=preview
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/install.sh | sed 's@BRANCH=master@BRANCH='"$RELEASE"'@' > setup.sh
|
||||
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Proceed with setup
|
||||
@@ -114,7 +97,7 @@ This will create a create a folder `plane-app` or `plane-app-preview` (in case o
|
||||
- `docker-compose.yaml`
|
||||
- `plane.env`
|
||||
|
||||
Again the `options [1-7]` will be popped up and this time hit `7` to exit.
|
||||
Again the `options [1-8]` will be popped up and this time hit `8` to exit.
|
||||
|
||||
---
|
||||
|
||||
@@ -236,7 +219,7 @@ Select a Action you want to perform:
|
||||
Action [2]: 5
|
||||
```
|
||||
|
||||
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `variables-upgrade.env`. Here system will not replace `.env` with the new one.
|
||||
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`.
|
||||
|
||||
You must expect the below message
|
||||
|
||||
@@ -244,7 +227,7 @@ You must expect the below message
|
||||
|
||||
Once done, choose `8` to exit from prompt.
|
||||
|
||||
> It is very important for you to compare the 2 files `variables-upgrade.env` and `.env`. Copy the newly added variable from downloaded file to `.env` and set the expected values.
|
||||
> It is very important for you to validate the `plane.env` for the new changes.
|
||||
|
||||
Once done with making changes in `plane.env` file, jump on to `Start Server`
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest}
|
||||
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: node web/server.js web
|
||||
deploy:
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: node space/server.js space
|
||||
deploy:
|
||||
@@ -71,7 +71,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: node admin/server.js admin
|
||||
deploy:
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: ./bin/docker-entrypoint-api.sh
|
||||
deploy:
|
||||
@@ -99,7 +99,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
volumes:
|
||||
@@ -113,7 +113,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
volumes:
|
||||
@@ -127,7 +127,7 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: "no"
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
volumes:
|
||||
@@ -167,7 +167,8 @@ services:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
depends_on:
|
||||
|
||||
+249
-131
@@ -1,18 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
BRANCH=master
|
||||
BRANCH=${BRANCH:-master}
|
||||
SCRIPT_DIR=$PWD
|
||||
SERVICE_FOLDER=plane-app
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
export APP_RELEASE=$BRANCH
|
||||
export APP_RELEASE="stable"
|
||||
export DOCKERHUB_USER=makeplane
|
||||
export PULL_POLICY=always
|
||||
USE_GLOBAL_IMAGES=1
|
||||
export PULL_POLICY=${PULL_POLICY:-if_not_present}
|
||||
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m' # No Color
|
||||
CPU_ARCH=$(uname -m)
|
||||
|
||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||
|
||||
function print_header() {
|
||||
clear
|
||||
@@ -31,65 +31,191 @@ Project management tool from the future
|
||||
EOF
|
||||
}
|
||||
|
||||
function buildLocalImage() {
|
||||
if [ "$1" == "--force-build" ]; then
|
||||
DO_BUILD="1"
|
||||
elif [ "$1" == "--skip-build" ]; then
|
||||
DO_BUILD="2"
|
||||
else
|
||||
printf "\n" >&2
|
||||
printf "${YELLOW}You are on ${CPU_ARCH} cpu architecture. ${NC}\n" >&2
|
||||
printf "${YELLOW}Since the prebuilt ${CPU_ARCH} compatible docker images are not available for, we will be running the docker build on this system. ${NC} \n" >&2
|
||||
printf "${YELLOW}This might take ${YELLOW}5-30 min based on your system's hardware configuration. \n ${NC} \n" >&2
|
||||
printf "\n" >&2
|
||||
printf "${GREEN}Select an option to proceed: ${NC}\n" >&2
|
||||
printf " 1) Build Fresh Images \n" >&2
|
||||
printf " 2) Skip Building Images \n" >&2
|
||||
printf " 3) Exit \n" >&2
|
||||
printf "\n" >&2
|
||||
read -p "Select Option [1]: " DO_BUILD
|
||||
until [[ -z "$DO_BUILD" || "$DO_BUILD" =~ ^[1-3]$ ]]; do
|
||||
echo "$DO_BUILD: invalid selection." >&2
|
||||
read -p "Select Option [1]: " DO_BUILD
|
||||
done
|
||||
function spinner() {
|
||||
local pid=$1
|
||||
local delay=.5
|
||||
local spinstr='|/-\'
|
||||
|
||||
if ! ps -p "$pid" > /dev/null; then
|
||||
echo "Invalid PID: $pid"
|
||||
return 1
|
||||
fi
|
||||
while ps -p "$pid" > /dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf " [%c] " "$spinstr" >&2
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep $delay
|
||||
printf "\b\b\b\b\b\b" >&2
|
||||
done
|
||||
printf " \b\b\b\b" >&2
|
||||
}
|
||||
|
||||
function initialize(){
|
||||
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${CPU_ARCH^^} support." >&2
|
||||
|
||||
if [ "$CUSTOM_BUILD" == "true" ]; then
|
||||
echo "" >&2
|
||||
echo "" >&2
|
||||
echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2
|
||||
echo "build"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$DO_BUILD" == "1" ] || [ "$DO_BUILD" == "" ];
|
||||
then
|
||||
REPO=https://github.com/makeplane/plane.git
|
||||
CURR_DIR=$PWD
|
||||
PLANE_TEMP_CODE_DIR=$(mktemp -d)
|
||||
git clone $REPO $PLANE_TEMP_CODE_DIR --branch $BRANCH --single-branch
|
||||
local IMAGE_NAME=makeplane/plane-proxy
|
||||
local IMAGE_TAG=${APP_RELEASE}
|
||||
docker manifest inspect "${IMAGE_NAME}:${IMAGE_TAG}" | grep -q "\"architecture\": \"${CPU_ARCH}\"" &
|
||||
local pid=$!
|
||||
spinner "$pid"
|
||||
|
||||
echo "" >&2
|
||||
|
||||
cp $PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml $PLANE_TEMP_CODE_DIR/build.yml
|
||||
wait "$pid"
|
||||
|
||||
cd $PLANE_TEMP_CODE_DIR
|
||||
if [ "$BRANCH" == "master" ];
|
||||
then
|
||||
export APP_RELEASE=stable
|
||||
fi
|
||||
|
||||
/bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
|
||||
# cd $CURR_DIR
|
||||
# rm -rf $PLANE_TEMP_CODE_DIR
|
||||
echo "build_completed"
|
||||
elif [ "$DO_BUILD" == "2" ];
|
||||
then
|
||||
printf "${YELLOW}Build action skipped by you in lieu of using existing images. ${NC} \n" >&2
|
||||
echo "build_skipped"
|
||||
elif [ "$DO_BUILD" == "3" ];
|
||||
then
|
||||
echo "build_exited"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Plane supports ${CPU_ARCH}" >&2
|
||||
echo "available"
|
||||
return 0
|
||||
else
|
||||
printf "INVALID OPTION SUPPLIED" >&2
|
||||
echo "" >&2
|
||||
echo "" >&2
|
||||
echo "${CPU_ARCH^^} images are not available for selected release ($APP_RELEASE)." >&2
|
||||
echo "" >&2
|
||||
echo "build"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
function install() {
|
||||
echo "Installing Plane.........."
|
||||
download
|
||||
function getEnvValue() {
|
||||
local key=$1
|
||||
local file=$2
|
||||
|
||||
if [ -z "$key" ] || [ -z "$file" ]; then
|
||||
echo "Invalid arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$file" ]; then
|
||||
grep -q "^$key=" "$file"
|
||||
if [ $? -eq 0 ]; then
|
||||
local value
|
||||
value=$(grep "^$key=" "$file" | cut -d'=' -f2)
|
||||
echo "$value"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
}
|
||||
function updateEnvFile() {
|
||||
local key=$1
|
||||
local value=$2
|
||||
local file=$3
|
||||
|
||||
if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then
|
||||
echo "Invalid arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$file" ]; then
|
||||
# check if key exists in the file
|
||||
grep -q "^$key=" "$file"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$key=$value" >> "$file"
|
||||
return
|
||||
else
|
||||
# if key exists, update the value
|
||||
sed -i "s/^$key=.*/$key=$value/g" "$file"
|
||||
fi
|
||||
else
|
||||
echo "File not found: $file"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function updateCustomVariables(){
|
||||
echo "Updating custom variables..." >&2
|
||||
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
|
||||
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
|
||||
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
|
||||
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
|
||||
echo "Custom variables updated successfully" >&2
|
||||
}
|
||||
|
||||
function syncEnvFile(){
|
||||
echo "Syncing environment variables..." >&2
|
||||
if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then
|
||||
updateCustomVariables
|
||||
|
||||
# READ keys of plane.env and update the values from plane.env.bak
|
||||
while IFS= read -r line
|
||||
do
|
||||
# ignore is the line is empty or starts with #
|
||||
if [ -z "$line" ] || [[ $line == \#* ]]; then
|
||||
continue
|
||||
fi
|
||||
key=$(echo "$line" | cut -d'=' -f1)
|
||||
value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak")
|
||||
if [ -n "$value" ]; then
|
||||
updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
done < "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
echo "Environment variables synced successfully" >&2
|
||||
}
|
||||
|
||||
function buildYourOwnImage(){
|
||||
echo "Building images locally..."
|
||||
|
||||
export DOCKERHUB_USER="myplane"
|
||||
export APP_RELEASE="local"
|
||||
export PULL_POLICY="never"
|
||||
CUSTOM_BUILD="true"
|
||||
|
||||
# checkout the code to ~/tmp/plane folder and build the images
|
||||
local PLANE_TEMP_CODE_DIR=~/tmp/plane
|
||||
rm -rf $PLANE_TEMP_CODE_DIR
|
||||
mkdir -p $PLANE_TEMP_CODE_DIR
|
||||
REPO=https://github.com/makeplane/plane.git
|
||||
git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1
|
||||
|
||||
cp "$PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml"
|
||||
|
||||
cd "$PLANE_TEMP_CODE_DIR" || exit
|
||||
|
||||
/bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
echo "Build completed successfully"
|
||||
echo ""
|
||||
echo "You can now start the services by running the command: ./setup.sh start"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function install() {
|
||||
echo "Begin Installing Plane"
|
||||
echo ""
|
||||
|
||||
local build_image=$(initialize)
|
||||
|
||||
if [ "$build_image" == "build" ]; then
|
||||
# ask for confirmation to continue building the images
|
||||
echo "Do you want to continue with building the Docker images locally?"
|
||||
read -p "Continue? [y/N]: " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "Exiting..."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$build_image" == "build" ]; then
|
||||
download "true"
|
||||
else
|
||||
download "false"
|
||||
fi
|
||||
}
|
||||
|
||||
function download() {
|
||||
local LOCAL_BUILD=$1
|
||||
cd $SCRIPT_DIR
|
||||
TS=$(date +%s)
|
||||
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
|
||||
@@ -102,44 +228,48 @@ function download() {
|
||||
|
||||
if [ -f "$DOCKER_ENV_PATH" ];
|
||||
then
|
||||
cp $DOCKER_ENV_PATH $PLANE_INSTALL_DIR/archive/$TS.env
|
||||
else
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
|
||||
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
|
||||
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
|
||||
fi
|
||||
|
||||
if [ "$BRANCH" != "master" ];
|
||||
then
|
||||
cp $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/temp.yaml
|
||||
sed -e 's@${APP_RELEASE:-stable}@'"$BRANCH"'@g' \
|
||||
$PLANE_INSTALL_DIR/temp.yaml > $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
|
||||
|
||||
rm $PLANE_INSTALL_DIR/temp.yaml
|
||||
fi
|
||||
syncEnvFile
|
||||
|
||||
if [ $USE_GLOBAL_IMAGES == 0 ]; then
|
||||
local res=$(buildLocalImage)
|
||||
# echo $res
|
||||
if [ "$LOCAL_BUILD" == "true" ]; then
|
||||
export DOCKERHUB_USER="myplane"
|
||||
export APP_RELEASE="local"
|
||||
export PULL_POLICY="never"
|
||||
CUSTOM_BUILD="true"
|
||||
|
||||
if [ "$res" == "build_exited" ];
|
||||
then
|
||||
echo
|
||||
echo "Install action cancelled by you. Exiting now."
|
||||
echo
|
||||
exit 0
|
||||
buildYourOwnImage
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Build failed. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
updateCustomVariables
|
||||
else
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull"
|
||||
CUSTOM_BUILD="false"
|
||||
updateCustomVariables
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull --policy always"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Failed to pull the images. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Most recent Stable version is now available for you to use"
|
||||
echo "Most recent version of Plane is now available for you to use"
|
||||
echo ""
|
||||
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in 'plane.env 'file."
|
||||
echo "In case of 'Upgrade', please check the 'plane.env 'file for any new variables and update them accordingly"
|
||||
echo ""
|
||||
|
||||
}
|
||||
function startServices() {
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --quiet-pull"
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --pull if_not_present --quiet-pull"
|
||||
|
||||
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
|
||||
if [ -n "$migrator_container_id" ]; then
|
||||
@@ -201,7 +331,7 @@ function upgrade() {
|
||||
|
||||
echo
|
||||
echo "***** DOWNLOADING STABLE VERSION ****"
|
||||
download
|
||||
install
|
||||
|
||||
echo "***** PLEASE VALIDATE AND START SERVICES ****"
|
||||
}
|
||||
@@ -282,7 +412,6 @@ function viewLogs(){
|
||||
echo "INVALID SERVICE NAME SUPPLIED"
|
||||
fi
|
||||
}
|
||||
|
||||
function backupSingleVolume() {
|
||||
backupFolder=$1
|
||||
selectedVolume=$2
|
||||
@@ -299,7 +428,6 @@ function backupSingleVolume() {
|
||||
-v "$backupFolder":/backup \
|
||||
busybox sh -c 'tar -czf "/backup/${TAR_NAME}.tar.gz" /${TAR_NAME}'
|
||||
}
|
||||
|
||||
function backupData() {
|
||||
local datetime=$(date +"%Y%m%d-%H%M")
|
||||
local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime
|
||||
@@ -329,7 +457,7 @@ function askForAction() {
|
||||
then
|
||||
echo
|
||||
echo "Select a Action you want to perform:"
|
||||
echo " 1) Install (${CPU_ARCH})"
|
||||
echo " 1) Install"
|
||||
echo " 2) Start"
|
||||
echo " 3) Stop"
|
||||
echo " 4) Restart"
|
||||
@@ -351,31 +479,31 @@ function askForAction() {
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ]
|
||||
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ];
|
||||
then
|
||||
install
|
||||
askForAction
|
||||
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ]
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ];
|
||||
then
|
||||
startServices
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ]
|
||||
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ];
|
||||
then
|
||||
stopServices
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ]
|
||||
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ];
|
||||
then
|
||||
restartServices
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ]
|
||||
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ];
|
||||
then
|
||||
upgrade
|
||||
askForAction
|
||||
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ]
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ];
|
||||
then
|
||||
viewLogs $@
|
||||
viewLogs "$@"
|
||||
askForAction
|
||||
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ]
|
||||
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ];
|
||||
then
|
||||
backupData
|
||||
elif [ "$ACTION" == "8" ]
|
||||
@@ -394,48 +522,38 @@ else
|
||||
COMPOSE_CMD="docker compose"
|
||||
fi
|
||||
|
||||
# CPU ARCHITECHTURE BASED SETTINGS
|
||||
CPU_ARCH=$(uname -m)
|
||||
if [[ $FORCE_CPU == "amd64" || $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]];
|
||||
then
|
||||
USE_GLOBAL_IMAGES=1
|
||||
DOCKERHUB_USER=makeplane
|
||||
PULL_POLICY=always
|
||||
else
|
||||
USE_GLOBAL_IMAGES=0
|
||||
DOCKERHUB_USER=myplane
|
||||
PULL_POLICY=never
|
||||
if [ "$CPU_ARCH" == "x86_64" ] || [ "$CPU_ARCH" == "amd64" ]; then
|
||||
CPU_ARCH="amd64"
|
||||
elif [ "$CPU_ARCH" == "aarch64" ] || [ "$CPU_ARCH" == "arm64" ]; then
|
||||
CPU_ARCH="arm64"
|
||||
fi
|
||||
|
||||
if [ "$BRANCH" == "master" ];
|
||||
then
|
||||
export APP_RELEASE=stable
|
||||
fi
|
||||
if [ -f "$DOCKER_ENV_PATH" ]; then
|
||||
DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH")
|
||||
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
|
||||
PULL_POLICY=$(getEnvValue "PULL_POLICY" "$DOCKER_ENV_PATH")
|
||||
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
|
||||
|
||||
# REMOVE SPECIAL CHARACTERS FROM BRANCH NAME
|
||||
if [ "$BRANCH" != "master" ];
|
||||
then
|
||||
SERVICE_FOLDER=plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g')
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
fi
|
||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
if [ -z "$DOCKERHUB_USER" ]; then
|
||||
DOCKERHUB_USER=makeplane
|
||||
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||
if [ -z "$APP_RELEASE" ]; then
|
||||
APP_RELEASE=stable
|
||||
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
|
||||
# BACKWARD COMPATIBILITY
|
||||
OLD_DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/.env
|
||||
if [ -f "$OLD_DOCKER_ENV_PATH" ];
|
||||
then
|
||||
mv "$OLD_DOCKER_ENV_PATH" "$DOCKER_ENV_PATH"
|
||||
OS_NAME=$(uname)
|
||||
if [ "$OS_NAME" == "Darwin" ];
|
||||
then
|
||||
sed -i '' -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
|
||||
else
|
||||
sed -i -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
|
||||
if [ -z "$PULL_POLICY" ]; then
|
||||
PULL_POLICY=if_not_present
|
||||
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
|
||||
if [ -z "$CUSTOM_BUILD" ]; then
|
||||
CUSTOM_BUILD=false
|
||||
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
print_header
|
||||
askForAction $@
|
||||
askForAction "$@"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
APP_DOMAIN=localhost
|
||||
APP_RELEASE=stable
|
||||
|
||||
WEB_REPLICAS=1
|
||||
@@ -6,11 +7,11 @@ ADMIN_REPLICAS=1
|
||||
API_REPLICAS=1
|
||||
|
||||
NGINX_PORT=80
|
||||
WEB_URL=http://localhost
|
||||
WEB_URL=http://${APP_DOMAIN}
|
||||
DEBUG=0
|
||||
SENTRY_DSN=
|
||||
SENTRY_ENVIRONMENT=production
|
||||
CORS_ALLOWED_ORIGINS=http://localhost
|
||||
CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
|
||||
|
||||
#DB SETTINGS
|
||||
PGHOST=plane-db
|
||||
@@ -46,4 +47,5 @@ FILE_SIZE_LIMIT=5242880
|
||||
GUNICORN_WORKERS=1
|
||||
|
||||
# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
|
||||
# DOCKER_PLATFORM=linux/amd64
|
||||
# DOCKER_PLATFORM=linux/amd64
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export const EnterKeyExtension = (onEnterKeyPress?: (descriptionHTML: string) =>
|
||||
editor.commands.first(({ commands }) => [
|
||||
() => commands.newlineInCode(),
|
||||
() => commands.splitListItem("listItem"),
|
||||
() => commands.splitListItem("taskItem"),
|
||||
() => commands.createParagraphNear(),
|
||||
() => commands.liftEmptyBlock(),
|
||||
() => commands.splitBlock(),
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
return (
|
||||
<>
|
||||
<Moveable
|
||||
target={document.querySelector(".ProseMirror-selectednode") as HTMLElement}
|
||||
target={document.querySelector(".active-editor .ProseMirror-selectednode") as HTMLElement}
|
||||
container={null}
|
||||
origin={false}
|
||||
edge={false}
|
||||
@@ -39,7 +39,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
resizable
|
||||
throttleResize={0}
|
||||
onResizeStart={() => {
|
||||
const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
|
||||
const imageInfo = document.querySelector(".active-editor .ProseMirror-selectednode") as HTMLImageElement;
|
||||
if (imageInfo) {
|
||||
const originalWidth = Number(imageInfo.width);
|
||||
const originalHeight = Number(imageInfo.height);
|
||||
|
||||
Vendored
+1
-1
@@ -42,7 +42,7 @@ export type TBaseIssue = {
|
||||
export type TIssue = TBaseIssue & {
|
||||
description_html?: string;
|
||||
is_subscribed?: boolean;
|
||||
parent?: partial<TIssue>;
|
||||
parent?: Partial<TBaseIssue>;
|
||||
issue_reactions?: TIssueReaction[];
|
||||
issue_attachment?: TIssueAttachment[];
|
||||
issue_link?: TIssueLink[];
|
||||
|
||||
@@ -6,6 +6,7 @@ import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { IssuesNavbarRoot } from "@/components/issues";
|
||||
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
|
||||
// hooks
|
||||
import { useIssueFilter, usePublish, usePublishList } from "@/hooks/store";
|
||||
// assets
|
||||
@@ -27,7 +28,7 @@ const IssuesLayout = observer((props: Props) => {
|
||||
const publishSettings = usePublish(anchor);
|
||||
const { updateLayoutOptions } = useIssueFilter();
|
||||
// fetch publish settings
|
||||
useSWR(
|
||||
const { error } = useSWR(
|
||||
anchor ? `PUBLISH_SETTINGS_${anchor}` : null,
|
||||
anchor
|
||||
? async () => {
|
||||
@@ -45,7 +46,9 @@ const IssuesLayout = observer((props: Props) => {
|
||||
: null
|
||||
);
|
||||
|
||||
if (!publishSettings) return <LogoSpinner />;
|
||||
if (!publishSettings && !error) return <LogoSpinner />;
|
||||
|
||||
if (error) return <SomethingWentWrongError />;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
|
||||
|
||||
@@ -5,6 +5,7 @@ import Image from "next/image";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
|
||||
// hooks
|
||||
import { usePublish, usePublishList } from "@/hooks/store";
|
||||
// Plane web
|
||||
@@ -28,25 +29,22 @@ const IssuesLayout = observer((props: Props) => {
|
||||
const { fetchPublishSettings } = usePublishList();
|
||||
const { viewData, fetchViewDetails } = useView();
|
||||
const publishSettings = usePublish(anchor);
|
||||
// fetch publish settings
|
||||
useSWR(
|
||||
anchor ? `PUBLISH_SETTINGS_${anchor}` : null,
|
||||
|
||||
// fetch publish settings && view details
|
||||
const { error } = useSWR(
|
||||
anchor ? `PUBLISHED_VIEW_SETTINGS_${anchor}` : null,
|
||||
anchor
|
||||
? async () => {
|
||||
await fetchPublishSettings(anchor);
|
||||
}
|
||||
: null
|
||||
);
|
||||
// fetch view data
|
||||
useSWR(
|
||||
anchor ? `VIEW_DETAILS_${anchor}` : null,
|
||||
anchor
|
||||
? async () => {
|
||||
await fetchViewDetails(anchor);
|
||||
const promises = [];
|
||||
promises.push(fetchPublishSettings(anchor));
|
||||
promises.push(fetchViewDetails(anchor));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
: null
|
||||
);
|
||||
|
||||
if (error) return <SomethingWentWrongError />;
|
||||
|
||||
if (!publishSettings || !viewData) return <LogoSpinner />;
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import Image from "next/image";
|
||||
// assets
|
||||
import SomethingWentWrongImage from "public/something-went-wrong.svg";
|
||||
|
||||
export const SomethingWentWrongError = () => (
|
||||
<div className="grid h-full w-full place-items-center p-6">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
|
||||
<div className="grid h-32 w-32 place-items-center">
|
||||
<Image src={SomethingWentWrongImage} alt="Oops! Something went wrong" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-12 text-3xl font-semibold">Oops! Something went wrong.</h1>
|
||||
<p className="mt-4 text-custom-text-300">The public board does not exist. Please check the URL.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { FC, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { IssueKanbanLayoutRoot, IssuesListLayoutRoot } from "@/components/issues";
|
||||
@@ -13,7 +12,7 @@ import { useIssue, useIssueDetails, useIssueFilter } from "@/hooks/store";
|
||||
// store
|
||||
import { PublishStore } from "@/store/publish/publish.store";
|
||||
// assets
|
||||
import SomethingWentWrongImage from "public/something-went-wrong.svg";
|
||||
import { SomethingWentWrongError } from "./error";
|
||||
|
||||
type Props = {
|
||||
peekId: string | undefined;
|
||||
@@ -24,7 +23,7 @@ export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
||||
const { peekId, publishSettings } = props;
|
||||
// store hooks
|
||||
const { getIssueFilters } = useIssueFilter();
|
||||
const { loader, groupedIssueIds, fetchPublicIssues } = useIssue();
|
||||
const { fetchPublicIssues } = useIssue();
|
||||
const issueDetailStore = useIssueDetails();
|
||||
// derived values
|
||||
const { anchor } = publishSettings;
|
||||
@@ -48,46 +47,27 @@ export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
||||
|
||||
if (!anchor) return null;
|
||||
|
||||
if (error) return <SomethingWentWrongError />;
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
{peekId && <IssuePeekOverview anchor={anchor} peekId={peekId} />}
|
||||
{activeLayout && (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{/* applied filters */}
|
||||
<IssueAppliedFilters anchor={anchor} />
|
||||
|
||||
{loader && !groupedIssueIds ? (
|
||||
<div className="py-10 text-center text-sm text-custom-text-100">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{error ? (
|
||||
<div className="grid h-full w-full place-items-center p-6">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
|
||||
<div className="grid h-32 w-32 place-items-center">
|
||||
<Image src={SomethingWentWrongImage} alt="Oops! Something went wrong" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-12 text-3xl font-semibold">Oops! Something went wrong.</h1>
|
||||
<p className="mt-4 text-custom-text-300">The public board does not exist. Please check the URL.</p>
|
||||
</div>
|
||||
{activeLayout === "list" && (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<IssuesListLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
) : (
|
||||
activeLayout && (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{/* applied filters */}
|
||||
<IssueAppliedFilters anchor={anchor} />
|
||||
|
||||
{activeLayout === "list" && (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<IssuesListLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
{activeLayout === "kanban" && (
|
||||
<div className="relative mx-auto h-full w-full p-5">
|
||||
<IssueKanbanLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
{activeLayout === "kanban" && (
|
||||
<div className="relative mx-auto h-full w-full p-5">
|
||||
<IssueKanbanLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -306,7 +306,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
...this.details[issueID].reaction_items,
|
||||
{
|
||||
reaction: reactionHex,
|
||||
actor_detail: this.rootStore.user.currentActor,
|
||||
actor_details: this.rootStore.user.currentActor,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ const WorkspaceDashboardPage = observer(() => {
|
||||
} = useUser();
|
||||
const { setPeekIssue } = useIssueDetail();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Inbox` : undefined;
|
||||
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
|
||||
notificationLiteByNotificationId(currentSelectedNotificationId);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ChevronDown, PanelRight } from "lucide-react";
|
||||
import { IUserProfileProjectSegregation } from "@plane/types";
|
||||
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
||||
import { Breadcrumbs, CustomMenu, UserActivityIcon } from "@plane/ui";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// components
|
||||
import { ProfileIssuesFilter } from "@/components/profile";
|
||||
@@ -43,14 +43,23 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||
|
||||
const isCurrentUser = currentUser?.id === userId;
|
||||
|
||||
const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Activity`;
|
||||
const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Work`;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<div className="flex w-full justify-between">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={breadcrumbLabel} disableTooltip />} />
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
label={breadcrumbLabel}
|
||||
disableTooltip
|
||||
icon={<UserActivityIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className="hidden md:flex md:items-center">{showProfileIssuesFilter && <ProfileIssuesFilter />}</div>
|
||||
<div className="flex gap-4 md:hidden">
|
||||
|
||||
@@ -59,8 +59,8 @@ const UseProfileLayout: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
{/* Passing the type prop from the current route value as we need the header as top most component.
|
||||
TODO: We are depending on the route path to handle the mobile header type. If the path changes, this logic will break. */}
|
||||
<div className="h-full w-full md:flex md:overflow-hidden">
|
||||
<div className="h-full w-full md:overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col md:flex-row overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<AppHeader
|
||||
header={
|
||||
<UserProfileHeader
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function ProfileOverviewPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title="Profile - Summary" />
|
||||
<PageHead title="Your work" />
|
||||
<div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9 vertical-scrollbar scrollbar-md">
|
||||
<ProfileStats userProfile={userProfile} />
|
||||
<ProfileWorkload stateDistribution={stateDistribution} />
|
||||
|
||||
@@ -36,7 +36,7 @@ const ProjectInboxPage = observer(() => {
|
||||
);
|
||||
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : "Plane - Inbox";
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Intake` : "Plane - Intake";
|
||||
|
||||
const currentNavigationTab = navigationTab
|
||||
? navigationTab === "open"
|
||||
|
||||
+22
-6
@@ -4,7 +4,7 @@ import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Earth, Layers, Lock } from "lucide-react";
|
||||
import { Layers, Lock } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
@@ -23,6 +23,7 @@ import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
import { getPublishViewLink } from "@/helpers/project-views.helpers";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import {
|
||||
@@ -132,6 +133,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
const publishLink = getPublishViewLink(viewDetails?.anchor);
|
||||
|
||||
return (
|
||||
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
|
||||
@@ -206,11 +208,25 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
<div className="cursor-default text-custom-text-300">
|
||||
<Tooltip tooltipContent={viewDetails?.access === EViewAccess.PUBLIC ? "Public" : "Private"}>
|
||||
{viewDetails?.access === EViewAccess.PUBLIC ? <Earth className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
|
||||
</Tooltip>
|
||||
</div>
|
||||
{viewDetails?.access === EViewAccess.PRIVATE && (
|
||||
<div className="cursor-default text-custom-text-300">
|
||||
<Tooltip tooltipContent={"Private"}>
|
||||
<Lock className="h-4 w-4" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewDetails?.anchor && publishLink && (
|
||||
<a
|
||||
href={publishLink}
|
||||
className="px-3 py-1.5 bg-green-500/20 text-green-500 rounded text-xs font-medium flex items-center gap-1.5"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="flex-shrink-0 rounded-full size-1.5 bg-green-500" />
|
||||
Live
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!viewDetails?.is_locked && (
|
||||
|
||||
@@ -91,7 +91,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||
onSubmit={handleWorkspaceInvite}
|
||||
/>
|
||||
<section className="w-full overflow-y-auto md:pr-9 pr-4">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 py-3.5">
|
||||
<div className="flex items-center justify-between gap-4 py-3.5">
|
||||
<h4 className="text-xl font-medium">Members</h4>
|
||||
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" />
|
||||
|
||||
@@ -9,23 +9,17 @@ import { GlobalViewsHeader } from "@/components/workspace";
|
||||
// constants
|
||||
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@/constants/workspace";
|
||||
// hooks
|
||||
import { useGlobalView, useWorkspace } from "@/hooks/store";
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
const GlobalViewIssuesPage = observer(() => {
|
||||
// router
|
||||
const { globalViewId } = useParams();
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { getViewDetailsById } = useGlobalView();
|
||||
|
||||
// derived values
|
||||
const globalViewDetails = globalViewId ? getViewDetailsById(globalViewId.toString()) : undefined;
|
||||
const defaultView = DEFAULT_GLOBAL_VIEWS_LIST.find((view) => view.key === globalViewId);
|
||||
const pageTitle =
|
||||
currentWorkspace?.name && defaultView?.label
|
||||
? `${currentWorkspace?.name} - ${defaultView?.label}`
|
||||
: currentWorkspace?.name && globalViewDetails?.name
|
||||
? `${currentWorkspace?.name} - ${globalViewDetails?.name}`
|
||||
: undefined;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - All Views` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,9 +4,10 @@ import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
import { Layers } from "lucide-react";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
|
||||
import { Breadcrumbs, Button } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
|
||||
@@ -108,9 +109,7 @@ export const GlobalIssuesHeader = observer(() => {
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink label={`All Issues`} icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
link={<BreadcrumbLink label={`Views`} icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
|
||||
@@ -369,7 +369,7 @@ const ProfileSettingsPage = observer(() => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors?.display_name && <span className="text-xs text-red-500">Please enter display name</span>}
|
||||
{errors?.display_name && <span className="text-xs text-red-500">{errors?.display_name?.message}</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -66,7 +66,6 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
onEnterKeyPress={(commentHTML) => {
|
||||
console.log("commentHTML", commentHTML);
|
||||
const isEmpty =
|
||||
commentHTML?.trim() === "" ||
|
||||
commentHTML === "<p></p>" ||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useEffect, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import type { TIssueLinkEditableFields } from "@plane/types";
|
||||
@@ -27,7 +28,7 @@ const defaultValues: TIssueLinkCreateFormFieldOptions = {
|
||||
url: "",
|
||||
};
|
||||
|
||||
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props) => {
|
||||
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observer((props) => {
|
||||
// props
|
||||
const { isModalOpen, handleOnClose, linkOperations } = props;
|
||||
|
||||
@@ -45,6 +46,7 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
|
||||
|
||||
const onClose = () => {
|
||||
setIssueLinkData(null);
|
||||
reset(defaultValues);
|
||||
if (handleOnClose) handleOnClose();
|
||||
};
|
||||
|
||||
@@ -55,8 +57,8 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset({ ...defaultValues, ...preloadedData });
|
||||
}, [preloadedData, reset]);
|
||||
if (isModalOpen) reset({ ...defaultValues, ...preloadedData });
|
||||
}, [preloadedData, reset, isModalOpen]);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isModalOpen} as={Fragment}>
|
||||
@@ -165,4 +167,4 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
+2
-1
@@ -117,7 +117,8 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const areFiltersEqual = getAreFiltersEqual(appliedFilters, issueFilters, viewDetails);
|
||||
// add a placeholder object instead of appliedFilters if it is undefined
|
||||
const areFiltersEqual = getAreFiltersEqual(appliedFilters ?? {}, issueFilters, viewDetails);
|
||||
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
|
||||
+2
-1
@@ -91,7 +91,8 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||
);
|
||||
};
|
||||
|
||||
const areFiltersEqual = getAreFiltersEqual(appliedFilters, issueFilters, viewDetails);
|
||||
// add a placeholder object instead of appliedFilters if it is undefined
|
||||
const areFiltersEqual = getAreFiltersEqual(appliedFilters ?? {}, issueFilters, viewDetails);
|
||||
const viewFilters = {
|
||||
filters: cloneDeep(appliedFilters ?? {}),
|
||||
display_filters: cloneDeep(issueFilters?.displayFilters),
|
||||
|
||||
@@ -222,7 +222,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
const kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] };
|
||||
|
||||
return (
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.KANBAN}>
|
||||
<>
|
||||
<DeleteIssueModal
|
||||
dataId={draggedIssueId}
|
||||
isOpen={deleteIssueModal}
|
||||
@@ -279,6 +279,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</IssueLayoutHOC>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -156,7 +156,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
column_id={subList.id}
|
||||
icon={subList.icon}
|
||||
title={subList.name}
|
||||
count={getGroupIssueCount(subList.id, undefined, false) ?? 0}
|
||||
count={getGroupIssueCount(subList.id, undefined, false)}
|
||||
issuePayload={subList.payload}
|
||||
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
||||
addIssuesToView={addIssuesToView}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { CreateUpdateIssueModal } from "@/components/issues";
|
||||
// hooks
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
import isNil from "lodash/isNil";
|
||||
// types
|
||||
|
||||
interface IHeaderGroupByCard {
|
||||
@@ -23,7 +24,7 @@ interface IHeaderGroupByCard {
|
||||
column_id: string;
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
count: number;
|
||||
count: number | undefined;
|
||||
kanbanFilters: TIssueKanbanFilters;
|
||||
handleKanbanFilters: any;
|
||||
issuePayload: Partial<TIssue>;
|
||||
@@ -112,7 +113,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`relative flex items-center gap-1 ${
|
||||
className={`relative flex items-baseline gap-1 ${
|
||||
verticalAlignPosition ? `flex-col` : `w-full flex-row overflow-hidden`
|
||||
}`}
|
||||
>
|
||||
@@ -123,11 +124,13 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
className={`flex-shrink-0 text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}
|
||||
>
|
||||
{count || 0}
|
||||
</div>
|
||||
{!isNil(count) && (
|
||||
<div
|
||||
className={`flex-shrink-0 text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}
|
||||
>
|
||||
{count || 0}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sub_group_by === null && (
|
||||
|
||||
@@ -28,11 +28,12 @@ import { GroupDragOverlay } from "../group-drag-overlay";
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { GroupDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload, getIssueBlockId } from "../utils";
|
||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||
import isNil from "lodash/isNil";
|
||||
|
||||
interface IKanbanGroup {
|
||||
groupId: string;
|
||||
issuesMap: IIssueMap;
|
||||
groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
|
||||
groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
sub_group_by: TIssueGroupByOptions | undefined;
|
||||
group_by: TIssueGroupByOptions | undefined;
|
||||
@@ -88,8 +89,8 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
|
||||
const containerRef = sub_group_by && scrollableContainerRef ? scrollableContainerRef : columnRef;
|
||||
|
||||
const loadMoreIssuesInThisGroup = useCallback(() => {
|
||||
loadMoreIssues(groupId, sub_group_id === "null"? undefined: sub_group_id)
|
||||
}, [loadMoreIssues, groupId, sub_group_id])
|
||||
loadMoreIssues(groupId, sub_group_id === "null" ? undefined : sub_group_id);
|
||||
}, [loadMoreIssues, groupId, sub_group_id]);
|
||||
|
||||
const isPaginating = !!getIssueLoader(groupId, sub_group_id);
|
||||
|
||||
@@ -218,11 +219,10 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
|
||||
? (groupedIssueIds as TSubGroupedIssues)?.[groupId]?.[sub_group_id] ?? []
|
||||
: (groupedIssueIds as TGroupedIssues)?.[groupId] ?? [];
|
||||
|
||||
const groupIssueCount = getGroupIssueCount(groupId, sub_group_id, false) ?? 0;
|
||||
const groupIssueCount = getGroupIssueCount(groupId, sub_group_id, false);
|
||||
|
||||
const nextPageResults = getPaginationData(groupId, sub_group_id)?.nextPageResults;
|
||||
|
||||
|
||||
const loadMore = isPaginating ? (
|
||||
<KanbanIssueBlockLoader />
|
||||
) : (
|
||||
@@ -235,8 +235,9 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults;
|
||||
const canOverlayBeVisible = orderBy !== "sort_order" || isDropDisabled;
|
||||
const shouldLoadMore =
|
||||
nextPageResults === undefined ? isNil(groupIssueCount) || issueIds?.length < groupIssueCount : !!nextPageResults;
|
||||
const canOverlayBeVisible = orderBy !== "sort_order" || isDropDisabled;
|
||||
const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
|
||||
|
||||
return (
|
||||
@@ -270,7 +271,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
|
||||
canDropOverIssue={!canOverlayBeVisible}
|
||||
/>
|
||||
|
||||
{shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)}
|
||||
{shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)}
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
|
||||
|
||||
@@ -56,9 +56,9 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((_list: IGroupByColumn) => {
|
||||
const groupCount = getGroupIssueCount(_list?.id, undefined, false) ?? 0;
|
||||
const groupCount = getGroupIssueCount(_list?.id, undefined, false);
|
||||
|
||||
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
|
||||
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount ?? 0, showEmptyGroup);
|
||||
|
||||
if (subGroupByVisibilityToggle === false) return <></>;
|
||||
|
||||
|
||||
@@ -107,27 +107,25 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.LIST}>
|
||||
<div className={`relative size-full bg-custom-background-90`}>
|
||||
<List
|
||||
issuesMap={issueMap}
|
||||
displayProperties={displayProperties}
|
||||
group_by={group_by}
|
||||
orderBy={orderBy}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={renderQuickActions}
|
||||
groupedIssueIds={groupedIssueIds ?? {}}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
quickAddCallback={quickAddIssue}
|
||||
enableIssueQuickAdd={!!enableQuickAdd}
|
||||
canEditProperties={canEditProperties}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
addIssuesToView={addIssuesToView}
|
||||
isCompletedCycle={isCompletedCycle}
|
||||
handleOnDrop={handleOnDrop}
|
||||
/>
|
||||
</div>
|
||||
</IssueLayoutHOC>
|
||||
<div className={`relative size-full bg-custom-background-90`}>
|
||||
<List
|
||||
issuesMap={issueMap}
|
||||
displayProperties={displayProperties}
|
||||
group_by={group_by}
|
||||
orderBy={orderBy}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={renderQuickActions}
|
||||
groupedIssueIds={groupedIssueIds ?? {}}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
quickAddCallback={quickAddIssue}
|
||||
enableIssueQuickAdd={!!enableQuickAdd}
|
||||
canEditProperties={canEditProperties}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
addIssuesToView={addIssuesToView}
|
||||
isCompletedCycle={isCompletedCycle}
|
||||
handleOnDrop={handleOnDrop}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -18,12 +18,13 @@ import { cn } from "@/helpers/common.helper";
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import isNil from "lodash/isNil";
|
||||
|
||||
interface IHeaderGroupByCard {
|
||||
groupID: string;
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
count: number;
|
||||
count: number | undefined;
|
||||
issuePayload: Partial<TIssue>;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
toggleListGroup: () => void;
|
||||
@@ -98,7 +99,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
||||
)}
|
||||
groupID={groupID}
|
||||
selectionHelpers={selectionHelpers}
|
||||
disabled={count === 0}
|
||||
disabled={!count}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -106,10 +107,12 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
||||
{icon ?? <CircleDashed className="size-3.5" strokeWidth={2} />}
|
||||
</div>
|
||||
|
||||
<div className="relative flex w-full flex-row items-center gap-1 overflow-hidden cursor-pointer"
|
||||
onClick={toggleListGroup}>
|
||||
<div
|
||||
className="relative flex w-full flex-row items-center gap-1 overflow-hidden cursor-pointer"
|
||||
onClick={toggleListGroup}
|
||||
>
|
||||
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{title}</div>
|
||||
<div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
|
||||
{!isNil(count) && <div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>}
|
||||
</div>
|
||||
|
||||
{!disableIssueCreation &&
|
||||
|
||||
@@ -37,6 +37,7 @@ import { IssueBlocksList } from "./blocks-list";
|
||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
import { ListQuickAddIssueForm } from "./quick-add-issue-form";
|
||||
import isNil from "lodash/isNil";
|
||||
|
||||
interface Props {
|
||||
groupIssueIds: string[] | undefined;
|
||||
@@ -98,7 +99,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
|
||||
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0;
|
||||
const groupIssueCount = getGroupIssueCount(group.id, undefined, false);
|
||||
const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults;
|
||||
const isPaginating = !!getIssueLoader(group.id);
|
||||
|
||||
@@ -106,7 +107,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
|
||||
const shouldLoadMore =
|
||||
nextPageResults === undefined && groupIssueCount !== undefined && groupIssueIds
|
||||
? groupIssueIds.length < groupIssueCount
|
||||
? isNil(groupIssueCount) || groupIssueIds.length < groupIssueCount
|
||||
: !!nextPageResults;
|
||||
|
||||
const loadMore = isPaginating ? (
|
||||
|
||||
@@ -29,7 +29,7 @@ const ProjectViewIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undef
|
||||
case EIssueLayoutTypes.CALENDAR:
|
||||
return <ProjectViewCalendarLayout />;
|
||||
case EIssueLayoutTypes.GANTT:
|
||||
return <BaseGanttRoot />;
|
||||
return <BaseGanttRoot viewId={props.viewId} />;
|
||||
case EIssueLayoutTypes.SPREADSHEET:
|
||||
return <ProjectViewSpreadsheetLayout />;
|
||||
default:
|
||||
|
||||
@@ -39,6 +39,7 @@ import { IMemberRootStore } from "@/store/member";
|
||||
import { IModuleStore } from "@/store/module.store";
|
||||
import { IProjectStore } from "@/store/project/project.store";
|
||||
import { IStateStore } from "@/store/state.store";
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
|
||||
export const HIGHLIGHT_CLASS = "highlight";
|
||||
export const HIGHLIGHT_WITH_LINE = "highlight-with-line";
|
||||
@@ -92,8 +93,10 @@ export const getGroupByColumns = (
|
||||
case "created_by":
|
||||
return getCreatedByColumns(member) as any;
|
||||
default:
|
||||
if (includeNone) return [{ id: `All Issues`, name: `All Issues`, payload: {}, icon: undefined }];
|
||||
if (includeNone) return [{ id: ALL_ISSUES, name: ALL_ISSUES, payload: {}, icon: undefined }];
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined => {
|
||||
|
||||
@@ -82,7 +82,7 @@ export const ProfileSidebar: FC<TProfileSidebar> = observer((props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`vertical-scrollbar scrollbar-md fixed z-[5] h-full w-full flex-shrink-0 overflow-hidden overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 shadow-custom-shadow-sm transition-all md:relative md:w-[300px]`,
|
||||
`vertical-scrollbar scrollbar-md fixed z-[5] h-full w-full flex-shrink-0 overflow-hidden overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 transition-all md:relative md:w-[300px]`,
|
||||
className
|
||||
)}
|
||||
style={profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Trash2 } from "lucide-react";
|
||||
@@ -6,7 +7,7 @@ import { IUser, IWorkspaceMember } from "@plane/types";
|
||||
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
|
||||
import { useMember } from "@/hooks/store";
|
||||
import { useMember, useUser } from "@/hooks/store";
|
||||
|
||||
export interface RowData {
|
||||
member: IWorkspaceMember;
|
||||
@@ -80,62 +81,74 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountTypeColumn: React.FC<AccountTypeProps> = (props) => {
|
||||
export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) => {
|
||||
const { rowData, currentProjectRole, projectId, workspaceSlug } = props;
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm();
|
||||
// store hooks
|
||||
const {
|
||||
project: { updateMember },
|
||||
} = useMember();
|
||||
return rowData.role === EUserWorkspaceRoles.ADMIN || currentProjectRole !== EUserProjectRoles.ADMIN ? (
|
||||
<div className="w-32 flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={(value: EUserProjectRoles) => {
|
||||
if (!workspaceSlug) return;
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
updateMember(workspaceSlug.toString(), projectId.toString(), rowData.member.id, {
|
||||
role: value as unknown as EUserProjectRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
|
||||
}).catch((err) => {
|
||||
console.log(err, "err");
|
||||
const error = err.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
// derived values
|
||||
const isCurrentUser = currentUser?.id === rowData.member.id;
|
||||
const isAdminRole = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const isRoleEditable = isCurrentUser && isAdminRole;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
label={
|
||||
<div className="flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
}
|
||||
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
||||
className="rounded-md p-0 w-32"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.keys(ROLE).map((item) => (
|
||||
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
|
||||
{ROLE[item as unknown as keyof typeof ROLE]}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
return (
|
||||
<>
|
||||
{isRoleEditable ? (
|
||||
<div className="w-32 flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={(value: EUserProjectRoles) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
updateMember(workspaceSlug.toString(), projectId.toString(), rowData.member.id, {
|
||||
role: value as unknown as EUserProjectRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
|
||||
}).catch((err) => {
|
||||
console.log(err, "err");
|
||||
const error = err.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
label={
|
||||
<div className="flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
}
|
||||
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
||||
className="rounded-md p-0 w-32"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.keys(ROLE).map((item) => (
|
||||
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
|
||||
{ROLE[item as unknown as keyof typeof ROLE]}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,11 +11,10 @@ export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { ca
|
||||
{cardsInEachColumn.map((cardsInColumn, columnIndex) => (
|
||||
<div key={columnIndex} className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between h-9 w-80">
|
||||
<div className="flex item-center">
|
||||
<div className="flex item-center gap-3 px-1.5">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded animate-pulse" />
|
||||
</div>
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
|
||||
</div>
|
||||
{Array.from({ length: cardsInColumn }, (_, cardIndex) => (
|
||||
<KanbanIssueBlockLoader key={cardIndex} />
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const MembersLayoutLoader = () => (
|
||||
<div className="flex gap-5 py-1.5 overflow-x-auto">
|
||||
{Array.from({ length: 5 }, (_, columnIndex) => (
|
||||
<div key={columnIndex} className="flex flex-col gap-3">
|
||||
<div className={`flex items-center justify-between h-9 ${columnIndex === 0 ? "w-80" : "w-36"}`}>
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded animate-pulse" />
|
||||
</div>
|
||||
{Array.from({ length: 2 }, (_, cardIndex) => (
|
||||
<span className="h-8 w-full bg-custom-background-80 rounded animate-pulse" key={cardIndex} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
+5
-5
@@ -31,12 +31,12 @@ export const NotificationFilterOptionItem: FC<{ label: string; value: ENotificat
|
||||
onClick={() => handleFilterTypeChange(value, !isSelected)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all",
|
||||
isSelected ? "bg-custom-primary-100" : "bg-custom-background-90"
|
||||
)}
|
||||
className={cn("flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all", {
|
||||
"bg-custom-primary text-white": isSelected,
|
||||
"bg-custom-background-90": !isSelected,
|
||||
})}
|
||||
>
|
||||
{isSelected && <Check className="h-2 w-2" />}
|
||||
{isSelected && <Check className="h-2.5 w-2.5" />}
|
||||
</div>
|
||||
<div className={cn("whitespace-nowrap text-sm", isSelected ? "text-custom-text-100" : "text-custom-text-200")}>
|
||||
{label}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Trash2 } from "lucide-react";
|
||||
@@ -6,7 +7,7 @@ import { IUser, IWorkspaceMember } from "@plane/types";
|
||||
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
|
||||
import { useMember } from "@/hooks/store";
|
||||
import { useMember, useUser } from "@/hooks/store";
|
||||
|
||||
export interface RowData {
|
||||
member: IWorkspaceMember;
|
||||
@@ -79,63 +80,75 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountTypeColumn: React.FC<AccountTypeProps> = (props) => {
|
||||
export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) => {
|
||||
const { rowData, currentWorkspaceRole, workspaceSlug } = props;
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm();
|
||||
// store hooks
|
||||
const {
|
||||
workspace: { updateMember },
|
||||
} = useMember();
|
||||
return rowData.role === EUserWorkspaceRoles.ADMIN || currentWorkspaceRole !== EUserWorkspaceRoles.ADMIN ? (
|
||||
<div className="w-32 flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={(value: EUserProjectRoles) => {
|
||||
console.log({ value, workspaceSlug }, "onChange");
|
||||
if (!workspaceSlug) return;
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
updateMember(workspaceSlug.toString(), rowData.member.id, {
|
||||
role: value as unknown as EUserWorkspaceRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
|
||||
}).catch((err) => {
|
||||
console.log(err, "err");
|
||||
const error = err.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
// derived values
|
||||
const isCurrentUser = currentUser?.id === rowData.member.id;
|
||||
const isAdminRole = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
const isRoleEditable = isCurrentUser && isAdminRole;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
label={
|
||||
<div className="flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
}
|
||||
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
||||
className="rounded-md p-0 w-32"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.keys(ROLE).map((item) => (
|
||||
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
|
||||
{ROLE[item as unknown as keyof typeof ROLE]}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
return (
|
||||
<>
|
||||
{isRoleEditable ? (
|
||||
<div className="w-32 flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={(value: EUserProjectRoles) => {
|
||||
console.log({ value, workspaceSlug }, "onChange");
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
updateMember(workspaceSlug.toString(), rowData.member.id, {
|
||||
role: value as unknown as EUserWorkspaceRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
|
||||
}).catch((err) => {
|
||||
console.log(err, "err");
|
||||
const error = err.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
label={
|
||||
<div className="flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
}
|
||||
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
||||
className="rounded-md p-0 w-32"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.keys(ROLE).map((item) => (
|
||||
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
|
||||
{ROLE[item as unknown as keyof typeof ROLE]}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { isEmpty } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { IWorkspaceMember } from "@plane/types";
|
||||
import { TOAST_TYPE, Table, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { MembersLayoutLoader } from "@/components/ui/loader/layouts/members-layout-loader";
|
||||
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace";
|
||||
// constants
|
||||
import { WORKSPACE_MEMBER_LEAVE } from "@/constants/event-tracker";
|
||||
@@ -79,8 +81,10 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
||||
// 2. only admin or member can change role
|
||||
// 3. user cannot change role of higher role
|
||||
|
||||
if (isEmpty(columns)) return <MembersLayoutLoader />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-t border-custom-border-100">
|
||||
{removeMemberModal && (
|
||||
<ConfirmWorkspaceMemberRemove
|
||||
isOpen={removeMemberModal.member.id.length > 0}
|
||||
@@ -93,7 +97,7 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
columns={columns}
|
||||
columns={columns ?? []}
|
||||
data={(memberDetails?.filter((member): member is IWorkspaceMember => member !== null) ?? []) as any}
|
||||
keyExtractor={(rowData) => rowData?.member.id ?? ""}
|
||||
tHeadClassName="border-b border-custom-border-100"
|
||||
@@ -102,6 +106,6 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
||||
tBodyTrClassName="divide-x-0"
|
||||
tHeadTrClassName="divide-x-0"
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useMember } from "@/hooks/store";
|
||||
|
||||
export const WorkspaceMembersList: FC<{ searchQuery: string; isAdmin: boolean }> = observer((props) => {
|
||||
const { searchQuery, isAdmin } = props;
|
||||
const [showPendingInvites, setShowPendingInvites] = useState<boolean>(false);
|
||||
const [showPendingInvites, setShowPendingInvites] = useState<boolean>(true);
|
||||
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
@@ -65,7 +65,7 @@ export const useGroupIssuesDragNDrop = (
|
||||
|
||||
if (isCycleChanged && workspaceSlug) {
|
||||
if (data[cycleKey]) {
|
||||
addCycleToIssue(workspaceSlug.toString(), projectId, data[cycleKey], issueId).catch(() =>
|
||||
addCycleToIssue(workspaceSlug.toString(), projectId, data[cycleKey]?.toString() ?? "", issueId).catch(() =>
|
||||
setToast(errorToastProps)
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -59,37 +59,37 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
// fetching project labels
|
||||
useSWR(
|
||||
const { isLoading: isLabelsLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_LABELS_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project members
|
||||
useSWR(
|
||||
const { isLoading: isMembersLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project states
|
||||
useSWR(
|
||||
const { isLoading: isStateLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project estimates
|
||||
useSWR(
|
||||
const { isLoading: isEstimatesLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => getProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project cycles
|
||||
useSWR(
|
||||
const { isLoading: isCyclesLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project modules
|
||||
useSWR(
|
||||
const { isLoading: isModulesLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_MODULES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchModules(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
@@ -102,8 +102,11 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
);
|
||||
const projectExists = projectId ? getProjectById(projectId.toString()) : null;
|
||||
|
||||
const isLoading =
|
||||
isLabelsLoading || isMembersLoading || isStateLoading || isEstimatesLoading || isCyclesLoading || isModulesLoading;
|
||||
|
||||
// check if the project member apis is loading
|
||||
if (!projectMemberInfo && projectId && hasPermissionToProject[projectId.toString()] === null)
|
||||
if ((!projectMemberInfo && projectId && hasPermissionToProject[projectId.toString()] === null) || isLoading)
|
||||
return (
|
||||
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
getSubGroupIssueKeyActions,
|
||||
} from "./base-issues-utils";
|
||||
import { IBaseIssueFilterStore } from "./issue-filter-helper.store";
|
||||
import { getGroupByColumns } from "@/components/issues/issue-layouts/utils";
|
||||
// constants
|
||||
// helpers
|
||||
// services
|
||||
@@ -419,6 +420,56 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
}
|
||||
);
|
||||
|
||||
getGroups = (isWorkspaceLevel = false) => {
|
||||
if (!this.groupBy || this.groupBy === "target_date") return;
|
||||
|
||||
const {
|
||||
projectRoot,
|
||||
cycle,
|
||||
module: moduleInfo,
|
||||
label,
|
||||
state: projectState,
|
||||
memberRoot,
|
||||
} = this.rootIssueStore.rootStore;
|
||||
|
||||
return getGroupByColumns(
|
||||
this.groupBy,
|
||||
projectRoot.project,
|
||||
cycle,
|
||||
moduleInfo,
|
||||
label,
|
||||
projectState,
|
||||
memberRoot,
|
||||
false,
|
||||
isWorkspaceLevel
|
||||
);
|
||||
};
|
||||
|
||||
getSubGroups = (isWorkspaceLevel = false) => {
|
||||
if (!this.subGroupBy || this.subGroupBy === "target_date") return;
|
||||
|
||||
const {
|
||||
projectRoot,
|
||||
cycle,
|
||||
module: moduleInfo,
|
||||
label,
|
||||
state: projectState,
|
||||
memberRoot,
|
||||
} = this.rootIssueStore.rootStore;
|
||||
|
||||
return getGroupByColumns(
|
||||
this.subGroupBy,
|
||||
projectRoot.project,
|
||||
cycle,
|
||||
moduleInfo,
|
||||
label,
|
||||
projectState,
|
||||
memberRoot,
|
||||
false,
|
||||
isWorkspaceLevel
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the next page cursor based on number of issues currently available
|
||||
* @param groupId groupId for the cursor
|
||||
@@ -451,9 +502,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
onfetchIssues(
|
||||
issuesResponse: TIssuesResponse,
|
||||
options: IssuePaginationOptions,
|
||||
workspaceSlug: string,
|
||||
projectId?: string,
|
||||
id?: string
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) {
|
||||
// Process the Issue Response to get the following data from it
|
||||
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
|
||||
@@ -463,15 +513,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
|
||||
// Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts
|
||||
runInAction(() => {
|
||||
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount);
|
||||
this.loader[getGroupKey()] = undefined;
|
||||
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount, groupId, subGroupId);
|
||||
this.loader[getGroupKey(groupId, subGroupId)] = undefined;
|
||||
});
|
||||
|
||||
// fetch parent stats if required, to be handled in the Implemented class
|
||||
this.fetchParentStats(workspaceSlug, projectId, id);
|
||||
|
||||
// store Pagination options for next subsequent calls and data like next cursor etc
|
||||
this.storePreviousPaginationValues(issuesResponse, options);
|
||||
this.storePreviousPaginationValues(issuesResponse, options, groupId, subGroupId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -892,9 +939,14 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
|
||||
addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
|
||||
|
||||
if (issueCycleId === cycleId) return;
|
||||
|
||||
try {
|
||||
// Update issueIds from current store
|
||||
runInAction(() => {
|
||||
// If cycle Id before update is the same as current cycle Id then, remove issueId from list
|
||||
if (this.cycleId === issueCycleId) this.removeIssueFromList(issueId);
|
||||
// If cycle Id is the current cycle Id, then, add issue to list of issueIds
|
||||
if (this.cycleId === cycleId) this.addIssueToList(issueId);
|
||||
// For Each issue update cycle Id by calling current store's update Issue, without making an API call
|
||||
|
||||
@@ -282,7 +282,7 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
||||
subGroupId?: string
|
||||
) {
|
||||
// if cursor exists, use the cursor. If it doesn't exist construct the cursor based on per page count
|
||||
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount}:1:0` : `${options.perPageCount}:0:0`;
|
||||
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount}:0:0` : `${options.perPageCount}:0:0`;
|
||||
|
||||
// pagination params
|
||||
const paginationParams: Partial<Record<TIssueParams, string | boolean>> = {
|
||||
|
||||
@@ -86,21 +86,74 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
});
|
||||
const groups = this.getGroups();
|
||||
const subGroups = this.getSubGroups();
|
||||
|
||||
this.clear(!isExistingPaginationOptions);
|
||||
|
||||
if (!groups || (!groups && !subGroups))
|
||||
return await this.fetchParallelIssues(workspaceSlug, projectId, loadType, options);
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const group of groups) {
|
||||
if (!subGroups) {
|
||||
promises.push(this.fetchParallelIssues(workspaceSlug, projectId, loadType, options, group.id));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const subGroup of subGroups) {
|
||||
promises.push(this.fetchParallelIssues(workspaceSlug, projectId, loadType, options, group.id, subGroup.id));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// fetch parent stats if required, to be handled in the Implemented class
|
||||
this.fetchParentStats(workspaceSlug, projectId);
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called to fetch the first issues of pagination
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
fetchParallelIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader = "init-loader",
|
||||
options: IssuePaginationOptions,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
// set Loader
|
||||
if (groupId || subGroupId) {
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
} else {
|
||||
this.setLoader(loadType);
|
||||
}
|
||||
});
|
||||
|
||||
// get params from pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(options, projectId, undefined, undefined, undefined);
|
||||
const params = this.issueFilterStore?.getFilterParams(options, projectId, undefined, groupId, subGroupId);
|
||||
// call the fetch issues API with the params
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params, {
|
||||
signal: this.controller.signal,
|
||||
});
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options, workspaceSlug, projectId);
|
||||
this.onfetchIssues(response, options, groupId, subGroupId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
|
||||
import set from "lodash/set";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// types
|
||||
import {
|
||||
@@ -184,7 +185,7 @@ export function getChangedIssuefields(formData: Partial<TIssue>, dirtyFields: {
|
||||
const dirtyFieldKeys = Object.keys(dirtyFields) as (keyof TIssue)[];
|
||||
for (const dirtyField of dirtyFieldKeys) {
|
||||
if (!!dirtyFields[dirtyField]) {
|
||||
changedFields[dirtyField] = formData[dirtyField];
|
||||
set(changedFields, [dirtyField], formData[dirtyField]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,4 +305,4 @@ export const getComputedDisplayProperties = (
|
||||
updated_on: displayProperties?.updated_on ?? true,
|
||||
modules: displayProperties?.modules ?? true,
|
||||
cycle: displayProperties?.cycle ?? true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import isNil from "lodash/isNil";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { IProjectView, TViewFilterProps, TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "./common.helper";
|
||||
import { satisfiesDateFilter } from "./filter.helper";
|
||||
|
||||
/**
|
||||
@@ -88,3 +89,15 @@ export const getValidatedViewFilters = (data: Partial<IProjectView>) => {
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns published view link
|
||||
* @param anchor
|
||||
* @returns
|
||||
*/
|
||||
export const getPublishViewLink = (anchor: string | undefined) => {
|
||||
if (!anchor) return;
|
||||
|
||||
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
|
||||
return `${SPACE_APP_URL}/views/${anchor}`;
|
||||
};
|
||||
Reference in New Issue
Block a user