Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
|
||||
|
||||
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>
|
||||
|
||||
+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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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` } : {}}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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