Compare commits

...

14 Commits

Author SHA1 Message Date
Bavisetti Narayan 2c609670c8 [WEB-2043] chore: updated permissions for delete operation (#5231)
* chore: added permission for delete operation

* chore: added permission for external apis

* chore: condition changes

* chore: minor changes
2024-07-26 16:42:51 +05:30
Akshita Goyal dfcba4dfc1 fix: revoked issue height change (#5238) 2024-07-26 13:38:26 +05:30
Manish Gupta d0e68cdcfb chore: self host custom build (#5228)
* removed code build process from install script

* fixes in install.sh

* fixed docker-compose.yaml

* wip

* sync env files during upgrade

* updated variables.env

* updated readme

* Update deploy/selfhost/install.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* implemented codacy suggestions

* implemented codacy suggestions

* Update deploy/selfhost/install.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update deploy/selfhost/install.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update deploy/selfhost/install.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* update codacy suggestions

* coderabbit suggestion

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-07-25 20:35:51 +05:30
Akshita Goyal 43103a1445 [WEB-2022] fix: handled null state on members page (#5226)
* fix: handled null state on members page

* fix: skeleton loader added
2024-07-25 16:28:03 +05:30
rahulramesha 1c155f6cbe fix view layout in space app (#5225) 2024-07-25 15:17:20 +05:30
rahulramesha 1707f4f282 Add live button on views (#5227) 2024-07-25 15:16:14 +05:30
rahulramesha c2c2ad0d7a fix project issue loader and error handling (#5223) 2024-07-25 14:15:16 +05:30
Akshita Goyal 1bf8f82ccb fix: flicker issue (#5210) 2024-07-25 13:55:29 +05:30
Anmol Singh Bhatia 3bdd91e577 [WEB-2053] fix: my work page scroll (#5224)
* fix: my work page scroll

* chore: profile sidebar shadow removed
2024-07-25 13:54:51 +05:30
rahulramesha 1f9c7a4b67 fix issue reactions in space app (#5222) 2024-07-24 20:34:03 +05:30
Akshita Goyal d1828c9496 [WEB-2040] fix: text updates (#5221)
* fix: text updates

* fix: page title

* fix: icon color

* fix: Page title changes
2024-07-24 20:30:52 +05:30
rahulramesha 3f87d8b99d fix gantt layout in project views (#5218) 2024-07-24 19:26:54 +05:30
rahulramesha aba6e603a3 fix view update button if no filters are applied (#5220) 2024-07-24 18:52:30 +05:30
Aaryan Khandelwal b4f2176ffa fix: issue parent type (#5219) 2024-07-24 18:34:07 +05:30
41 changed files with 607 additions and 310 deletions
+18 -3
View File
@@ -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",
+16 -19
View File
@@ -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)
+18 -2
View File
@@ -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
)
+15
View File
@@ -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
+18 -3
View File
@@ -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",
+16 -17
View File
@@ -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)
+28
View File
@@ -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):
+14
View File
@@ -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",
+16
View File
@@ -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
+14
View File
@@ -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(
+24 -8
View File
@@ -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(
+4 -21
View File
@@ -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`
-2
View File
@@ -1,5 +1,3 @@
version: "3.8"
services:
web:
image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest}
+9 -8
View File
@@ -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
View File
@@ -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 "$@"
+5 -3
View File
@@ -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
+1 -1
View File
@@ -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[];
+5 -2
View File
@@ -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">
+11 -13
View File
@@ -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>
);
+1 -1
View File
@@ -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"
@@ -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>
@@ -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;
@@ -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:
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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 {
+3 -2
View File
@@ -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,
});
});
+13
View File
@@ -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}`;
};