Compare commits

...
Author SHA1 Message Date
rahulramesha 6e0659cf47 local ssl config to enable the use of auth 2024-05-14 19:58:04 +05:30
ab6f1ef780 [WEB-1298] chore: project cycle revamp (#4454)
* chore: cycle endpoint changes

* chore: completed cycle icon updated

* chore: project cycle list revamp and code refactor

* chore: cycle page improvement

* chore: added created by in retrieve endopoint

* fix: build error

* chore: cycle list page disclosure button improvement

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-05-14 19:22:08 +05:30
sriram veeraghantaandGitHub febf19ccc0 feat: converting space app to use nextjs app dir (#4451)
* feat: changemod space

* fix: space app dir fixes

* fix: build errors
2024-05-14 14:26:54 +05:30
sriram veeraghanta 087d54a261 fix: worflow update 2024-05-14 14:26:23 +05:30
Bavisetti NarayanandGitHub c2ce3ada14 chore: update issue link (#4450) 2024-05-14 13:49:14 +05:30
dbc0d7019b [WEB - 1302]dev: nginx headers and auth error codes. (#4449)
* dev: add nginx headers

* fix: handled error messages in admin

---------

Co-authored-by: guru_sainath <gurusainath007@gmail.com>
2024-05-14 13:46:05 +05:30
Manish GuptaandGitHub 2593dc8afc added optional env FORCE_CPU and updated README (#4446) 2024-05-14 13:45:04 +05:30
Satish GandhamandGitHub 18ba4009e0 - Stop the default behavior on the custom menu button. (#4440)
- Refactor menu click handler function
2024-05-13 13:05:10 +05:30
Aaryan KhandelwalandGitHub 198a2a63f2 [WEB-1271] fix: show only joined projects in the filters list (#4417) 2024-05-13 12:06:34 +05:30
sriram veeraghanta 3723ece8d5 fix: postcss upgrade to latest version 2024-05-11 18:55:47 +05:30
sriram veeraghanta 91a66a757a fix: console warnings 2024-05-11 17:47:00 +05:30
Anmol Singh BhatiaandGitHub 4aed6e7aed fix: issue layout application error (#4437) 2024-05-11 16:29:53 +05:30
sriram veeraghanta 16d8dfc86e fix: build errors 2024-05-11 15:14:59 +05:30
Anmol Singh BhatiaandGitHub 3355be9c9c [WEB-1254] chore: list layout indentation enhancement and cycle list page ui improvement (#4435)
* chore: list layout indentation improvement

* chore: cycle list layout spacing and date ui updated

* chore: platform ui improvement
2024-05-11 14:47:56 +05:30
sriram veeraghanta 2ef3c06da0 fix: redirection issues and instance validation changes 2024-05-10 19:34:40 +05:30
164 changed files with 1882 additions and 2273 deletions
+16 -16
View File
@@ -14,10 +14,10 @@ jobs:
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
uses: tj-actions/changed-files@v44
with:
files_yaml: |
apiserver:
@@ -49,9 +49,9 @@ jobs:
runs-on: ubuntu-latest
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.x" # Specify the Python version you need
- name: Install Pylint
@@ -66,9 +66,9 @@ jobs:
if: needs.get-changed-files.outputs.admin_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: yarn install
@@ -79,9 +79,9 @@ jobs:
if: needs.get-changed-files.outputs.space_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: yarn install
@@ -92,9 +92,9 @@ jobs:
if: needs.get-changed-files.outputs.web_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: yarn install
@@ -104,9 +104,9 @@ jobs:
needs: lint-admin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: yarn install
@@ -116,9 +116,9 @@ jobs:
needs: lint-space
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: yarn install
@@ -128,9 +128,9 @@ jobs:
needs: lint-web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18.x
- run: yarn install
@@ -7,9 +7,10 @@ import { Transition } from "@headlessui/react";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// hooks
import { useInstance, useTheme } from "@/hooks/store";
import { useTheme } from "@/hooks/store";
// assets
import packageJson from "package.json";
import { WEB_BASE_URL } from "@/helpers/common.helper";
const helpOptions = [
{
@@ -30,8 +31,6 @@ const helpOptions = [
];
export const HelpSection: FC = observer(() => {
// hooks
const { instance } = useInstance();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store
@@ -39,7 +38,7 @@ export const HelpSection: FC = observer(() => {
// refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
return (
<div
+54 -53
View File
@@ -1,10 +1,12 @@
import { ReactNode } from "react";
import Link from "next/link";
export enum EPageTypes {
"PUBLIC" = "PUBLIC",
"NON_AUTHENTICATED" = "NON_AUTHENTICATED",
"ONBOARDING" = "ONBOARDING",
"AUTHENTICATED" = "AUTHENTICATED",
PUBLIC = "PUBLIC",
NON_AUTHENTICATED = "NON_AUTHENTICATED",
SET_PASSWORD = "SET_PASSWORD",
ONBOARDING = "ONBOARDING",
AUTHENTICATED = "AUTHENTICATED",
}
export enum EAuthModes {
@@ -18,28 +20,26 @@ export enum EAuthSteps {
UNIQUE_CODE = "UNIQUE_CODE",
}
export enum EAuthenticationErrorCodes {
INSTANCE_NOT_CONFIGURED = "5000",
// Admin
ADMIN_ALREADY_EXIST = "5029",
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5030",
INVALID_ADMIN_EMAIL = "5031",
INVALID_ADMIN_PASSWORD = "5032",
REQUIRED_ADMIN_EMAIL_PASSWORD = "5033",
ADMIN_AUTHENTICATION_FAILED = "5034",
ADMIN_USER_ALREADY_EXIST = "5035",
ADMIN_USER_DOES_NOT_EXIST = "5036",
}
export enum EErrorAlertType {
BANNER_ALERT = "BANNER_ALERT",
TOAST_ALERT = "TOAST_ALERT",
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
INLINE_EMAIL = "INLINE_EMAIL",
INLINE_PASSWORD = "INLINE_PASSWORD",
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
}
export enum EAuthenticationErrorCodes {
// Admin
ADMIN_ALREADY_EXIST = "5150",
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155",
INVALID_ADMIN_EMAIL = "5160",
INVALID_ADMIN_PASSWORD = "5165",
REQUIRED_ADMIN_EMAIL_PASSWORD = "5170",
ADMIN_AUTHENTICATION_FAILED = "5175",
ADMIN_USER_ALREADY_EXIST = "5180",
ADMIN_USER_DOES_NOT_EXIST = "5185",
}
export type TAuthErrorInfo = {
type: EErrorAlertType;
code: EAuthenticationErrorCodes;
@@ -50,41 +50,54 @@ export type TAuthErrorInfo = {
const errorCodeMessages: {
[key in EAuthenticationErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
} = {
[EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: {
title: "Instance not configured",
message: () => "Please contact your administrator to configure the instance.",
},
// admin
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
title: "Admin already exists",
message: () => "Admin already exists. Please sign in.",
title: `Admin already exists`,
message: () => `Admin already exists. Please try again.`,
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
title: "Required",
message: () => "Please enter email, password and first name.",
title: `Email, password and first name required`,
message: () => `Email, password and first name required. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
title: "Invalid email",
message: () => "Please enter a valid email.",
title: `Invalid admin email`,
message: () => `Invalid admin email. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
title: "Invalid password",
message: () => "Password must be at least 8 characters long.",
title: `Invalid admin password`,
message: () => `Invalid admin password. Please try again.`,
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
title: "Required",
message: () => "Please enter email and password.",
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
},
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
title: "Authentication failed",
message: () => "Please check your email and password and try again.",
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
},
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
title: "User already exists",
message: () => "User already exists. Please sign in.",
title: `Admin user already exists`,
message: () => (
<div>
Admin user already exists.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
title: "User does not exist",
message: () => "User does not exist. Please sign up.",
title: `Admin user does not exist`,
message: () => (
<div>
Admin user does not exist.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
};
@@ -92,28 +105,16 @@ export const authErrorHandler = (
errorCode: EAuthenticationErrorCodes,
email?: string | undefined
): TAuthErrorInfo | undefined => {
const toastAlertErrorCodes = [
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
];
const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
];
if (toastAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.TOAST_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
if (bannerAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.BANNER_ALERT,
+1 -1
View File
@@ -25,7 +25,7 @@
"mobx-react-lite": "^4.0.5",
"next": "^14.2.3",
"next-themes": "^0.2.1",
"postcss": "8.4.23",
"postcss": "^8.4.38",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.51.0",
+1 -1
View File
@@ -315,7 +315,7 @@ class IssueLinkSerializer(BaseSerializer):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
).exists():
).exclude(pk=instance.id).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
+1 -1
View File
@@ -462,7 +462,7 @@ class IssueLinkSerializer(BaseSerializer):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
).exists():
).exclude(pk=instance.id).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
+3
View File
@@ -241,6 +241,7 @@ class CycleViewSet(BaseViewSet):
"backlog_issues",
"assignee_ids",
"status",
"created_by"
)
if data:
@@ -365,6 +366,7 @@ class CycleViewSet(BaseViewSet):
"backlog_issues",
"assignee_ids",
"status",
"created_by",
)
return Response(data, status=status.HTTP_200_OK)
@@ -564,6 +566,7 @@ class CycleViewSet(BaseViewSet):
"backlog_issues",
"assignee_ids",
"status",
"created_by",
)
.first()
)
@@ -38,6 +38,8 @@ class InstanceEndpoint(BaseAPIView):
@cache_response(60 * 60 * 2, user=False)
def get(self, request):
instance = Instance.objects.first()
print("Instance: ", instance)
# get the instance
if instance is None:
return Response(
-13
View File
@@ -30,19 +30,6 @@ INTERNAL_IPS = ("127.0.0.1",)
MEDIA_URL = "/uploads/"
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") # noqa
CORS_ALLOWED_ORIGINS = [
"http://localhost",
"http://127.0.0.1",
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:3001",
"http://127.0.0.1:3001",
"http://localhost:3002",
"http://127.0.0.1:3002",
]
CSRF_TRUSTED_ORIGINS = CORS_ALLOWED_ORIGINS
CORS_ALLOW_ALL_ORIGINS = True
LOG_DIR = os.path.join(BASE_DIR, "logs") # noqa
if not os.path.exists(LOG_DIR):
+9
View File
@@ -21,6 +21,7 @@ from rest_framework.viewsets import ModelViewSet
# Module imports
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
from plane.authentication.session import BaseSessionAuthentication
class TimezoneMixin:
@@ -49,6 +50,10 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
SearchFilter,
)
authentication_classes = [
BaseSessionAuthentication,
]
filterset_fields = []
search_fields = []
@@ -146,6 +151,10 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
search_fields = []
authentication_classes = [
BaseSessionAuthentication,
]
def filter_queryset(self, queryset):
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
+2 -2
View File
@@ -96,7 +96,7 @@ This will prompt you with the below options.
```bash
Select a Action you want to perform:
1) Install (arm64)
1) Install (x86_64)
2) Start
3) Stop
4) Restart
@@ -344,7 +344,7 @@ Similarly, you can view the logs of other services.
There would a time when you might want to backup your data from docker volumes to external storage like S3 or drives.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `8` to view logs.
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `7` to Backup the data.
```bash
Select a Action you want to perform:
+1 -1
View File
@@ -390,7 +390,7 @@ fi
# CPU ARCHITECHTURE BASED SETTINGS
CPU_ARCH=$(uname -m)
if [[ $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]];
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
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
cd ./nginx
mkcert local.plane.so
mkcert local.plane.tools
+96
View File
@@ -0,0 +1,96 @@
events {
}
http {
sendfile on;
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate ./local.plane.so.pem;
ssl_certificate_key ./local.plane.so-key.pem;
server_name local.plane.so;
#client_max_body_size ${FILE_SIZE_LIMIT};
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Forwarded-Proto "$scheme";
add_header Host "$host";
location / {
proxy_pass http://localhost:3000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /god-mode/ {
proxy_pass http://localhost:3333/god-mode/;
}
location /spaces/ {
rewrite ^/spaces/?$ /spaces/login break;
proxy_pass http://localhost:4000/spaces/;
}
# location /${BUCKET_NAME}/ {
# proxy_pass http://plane-minio:9000/uploads/;
# }
# location /api/ {
# proxy_pass http://api:8000/api/;
# }
# location /auth/ {
# proxy_pass http://api:8000/auth/;
# }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate ./local.plane.tools.pem;
ssl_certificate_key ./local.plane.tools-key.pem;
server_name local.plane.tools;
#client_max_body_size ${FILE_SIZE_LIMIT};
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Forwarded-Proto "$scheme";
add_header Host "$host";
location / {
proxy_pass http://localhost:3000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /god-mode/ {
proxy_pass http://localhost:3333/god-mode/;
}
location /spaces/ {
rewrite ^/spaces/?$ /spaces/login break;
proxy_pass http://localhost:4000/spaces/;
}
# location /${BUCKET_NAME}/ {
# proxy_pass http://plane-minio:9000/uploads/;
# }
# location /api/ {
# proxy_pass http://api:8000/api/;
# }
# location /auth/ {
# proxy_pass http://api:8000/auth/;
# }
}
}
+7 -1
View File
@@ -16,7 +16,9 @@ http {
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Forwarded-Proto "${dollar}scheme";
add_header Host "${dollar}host";
add_header X-Forwarded-Host "${dollar}host";
add_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
add_header Host "${dollar}http_host";
location / {
proxy_pass http://web:3000/;
@@ -30,10 +32,14 @@ http {
}
location /api/ {
proxy_set_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
proxy_set_header Host "${dollar}http_host";
proxy_pass http://api:8000/api/;
}
location /auth/ {
proxy_set_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
proxy_set_header Host "${dollar}http_host";
proxy_pass http://api:8000/auth/;
}
+7 -1
View File
@@ -16,13 +16,17 @@ http {
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Forwarded-Proto "${dollar}scheme";
add_header Host "${dollar}host";
add_header X-Forwarded-Host "${dollar}host";
add_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
add_header Host "${dollar}http_host";
location / {
proxy_pass http://web:3000/;
}
location /api/ {
proxy_set_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
proxy_set_header Host "${dollar}http_host";
proxy_pass http://api:8000/api/;
}
@@ -35,6 +39,8 @@ http {
}
location /auth/ {
proxy_set_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
proxy_set_header Host "${dollar}http_host";
proxy_pass http://api:8000/auth/;
}
+3 -2
View File
@@ -17,6 +17,7 @@
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --concurrency=13",
"nginx-local": "nginx -c nginx/local.conf -p .",
"start": "turbo run start",
"lint": "turbo run lint",
"clean": "turbo run clean",
@@ -25,7 +26,7 @@
"devDependencies": {
"autoprefixer": "^10.4.15",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"postcss": "^8.4.38",
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"tailwindcss": "^3.3.3",
@@ -35,4 +36,4 @@
"@types/react": "18.2.48"
},
"packageManager": "yarn@1.22.19"
}
}
+1 -1
View File
@@ -61,7 +61,7 @@
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsconfig": "*",
"tsup": "^7.2.0",
+1 -1
View File
@@ -46,7 +46,7 @@
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsconfig": "*",
"tsup": "^7.2.0",
+1 -1
View File
@@ -42,7 +42,7 @@
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsconfig": "*",
"tsup": "^7.2.0",
@@ -37,7 +37,7 @@
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsconfig": "*",
"tsup": "^7.2.0",
@@ -39,7 +39,7 @@
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint-config-custom": "*",
"postcss": "^8.4.29",
"postcss": "^8.4.38",
"react": "^18.2.0",
"tailwind-config-custom": "*",
"tsconfig": "*",
+1 -1
View File
@@ -13,7 +13,7 @@ module.exports = {
plugins: ["react", "@typescript-eslint", "import"],
settings: {
next: {
rootDir: ["web/", "space/", "packages/*/"],
rootDir: ["web/", "space/", "admin/", "packages/*/"],
},
},
rules: {
+1 -1
View File
@@ -7,7 +7,7 @@
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"postcss": "^8.4.38",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.2.7",
+10 -17
View File
@@ -68,6 +68,13 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
if (closeOnSelect) closeDropdown();
};
const handleMenuButtonClick = (e:React.MouseEvent<HTMLButtonElement, MouseEvent>)=>{
e.stopPropagation();
e.preventDefault()
isOpen ? closeDropdown() : openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}
useOutsideClickDetector(dropdownRef, closeDropdown);
let menuItems = (
@@ -112,11 +119,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
<button
ref={setReferenceElement}
type="button"
onClick={(e) => {
e.stopPropagation();
isOpen ? closeDropdown() : openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}}
onClick={handleMenuButtonClick}
className={customButtonClassName}
tabIndex={customButtonTabIndex}
>
@@ -130,12 +133,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
<button
ref={setReferenceElement}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
isOpen ? closeDropdown() : openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}}
onClick={handleMenuButtonClick}
disabled={disabled}
className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none hover:text-custom-text-100 ${
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
@@ -157,12 +155,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
? "cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
isOpen ? closeDropdown() : openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}}
onClick={handleMenuButtonClick}
tabIndex={customButtonTabIndex}
>
{label}
@@ -3,8 +3,8 @@ import * as React from "react";
import { ISvgIcons } from "../type";
export const CircleDotFullIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg viewBox="0 0 16 16" className={`${className} stroke-1`} fill="none" xmlns="http://www.w3.org/2000/svg" {...rest}>
<circle cx="8.33333" cy="8.33333" r="5.33333" stroke="currentColor" strokeLinecap="round" />
<circle cx="8.33333" cy="8.33333" r="4.33333" fill="currentColor" />
<svg viewBox="0 0 24 24" className={`${className} stroke-1`} fill="none" xmlns="http://www.w3.org/2000/svg" {...rest}>
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" strokeLinecap="round" />
<circle cx="12" cy="12" r="6.25" fill="currentColor" stroke-width="0.5" />
</svg>
);
@@ -0,0 +1,5 @@
"use client";
export default function ProjectError() {
return <>Project Error</>;
}
@@ -0,0 +1,36 @@
import Image from "next/image";
// components
import IssueNavbar from "@/components/issues/navbar";
// services
import ProjectService from "@/services/project.service";
// assets
import planeLogo from "public/plane-logo.svg";
const projectService = new ProjectService();
export default async function ProjectLayout({ children, params }: { children: React.ReactNode; params: any }) {
const { workspace_slug, project_id } = params;
const projectSettings = await projectService.getProjectSettings(workspace_slug, project_id);
return (
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
<IssueNavbar projectSettings={projectSettings} workspaceSlug={workspace_slug} projectId={project_id} />
</div>
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
<a
href="https://plane.so"
className="fixed bottom-2.5 right-5 !z-[999999] flex items-center gap-1 rounded border border-custom-border-200 bg-custom-background-100 px-2 py-1 shadow-custom-shadow-2xs"
target="_blank"
rel="noreferrer noopener"
>
<div className="relative grid h-6 w-6 place-items-center">
<Image src={planeLogo} alt="Plane logo" className="h-6 w-6" height="24" width="24" />
</div>
<div className="text-xs">
Powered by <span className="font-semibold">Plane Deploy</span>
</div>
</a>
</div>
);
}
@@ -0,0 +1,3 @@
export default function ProjectSettingsNotFound() {
return <>Project Settings not found</>;
}
@@ -0,0 +1,8 @@
// components
import { ProjectDetailsView } from "@/components/views";
export default async function WorkspaceProjectPage({ params }: { params: any }) {
const { workspace_slug, project_id, peekId } = params;
return <ProjectDetailsView workspaceSlug={workspace_slug} projectId={project_id} peekId={peekId} />;
}
+40
View File
@@ -0,0 +1,40 @@
"use client";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Button } from "@plane/ui";
// assets
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
export default function InstanceError() {
const { resolvedTheme } = useTheme();
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
const handleRetry = () => {
window.location.reload();
};
return (
<div>
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center mt-10">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<Image src={instanceImage} alt="Plane Logo" />
<h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3>
<p className="font-medium text-base text-center">
We were unable to fetch the details of the instance. <br />
Fret not, it might just be a connectivity issue.
</p>
</div>
<div className="flex justify-center">
<Button size="md" onClick={handleRetry}>
Retry
</Button>
</div>
</div>
</div>
</div>
);
}
+35
View File
@@ -0,0 +1,35 @@
import { Metadata } from "next";
// styles
import "@/styles/globals.css";
// helpers
import { ASSET_PREFIX } from "@/helpers/common.helper";
export const metadata: Metadata = {
title: "Plane Deploy | Make your Plane boards public with one-click",
description: "Plane Deploy is a customer feedback management tool built on top of plane.so",
openGraph: {
title: "Plane Deploy | Make your Plane boards public with one-click",
description: "Plane Deploy is a customer feedback management tool built on top of plane.so",
url: "https://sites.plane.so/",
},
keywords:
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration",
twitter: {
site: "@planepowers",
},
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<link rel="apple-touch-icon" sizes="180x180" href={`${ASSET_PREFIX}favicon/apple-touch-icon.png`} />
<link rel="icon" type="image/png" sizes="32x32" href={`${ASSET_PREFIX}favicon/favicon-32x32.png`} />
<link rel="icon" type="image/png" sizes="16x16" href={`${ASSET_PREFIX}favicon/favicon-16x16.png`} />
<link rel="manifest" href={`${ASSET_PREFIX}site.webmanifest.json`} />
<link rel="shortcut icon" href={`${ASSET_PREFIX}favicon/favicon.ico`} />
</head>
<body>{children}</body>
</html>
);
}
+39
View File
@@ -0,0 +1,39 @@
"use client";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Button } from "@plane/ui";
// assets
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
export default function InstanceNotFound() {
const { resolvedTheme } = useTheme();
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
const handleRetry = () => {
window.location.reload();
};
return (
<div>
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center mt-10">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<Image src={instanceImage} alt="Plane Logo" />
<h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3>
<p className="font-medium text-base text-center">
We were unable to fetch the details of the instance. <br />
Fret not, it might just be a connectivity issue.
</p>
</div>
<div className="flex justify-center">
<Button size="md" onClick={handleRetry}>
Retry
</Button>
</div>
</div>
</div>
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
// components
import { UserLoggedIn } from "@/components/accounts";
import { InstanceNotReady, InstanceFailureView } from "@/components/instance";
import { AuthView } from "@/components/views";
// helpers
// import { EPageTypes } from "@/helpers/authentication.helper";
// import { useInstance, useUser } from "@/hooks/store";
// wrapper
// import { AuthWrapper } from "@/lib/wrappers";
// lib
import { AppProvider } from "@/lib/app-providers";
// services
import { InstanceService } from "@/services/instance.service";
import { UserService } from "@/services/user.service";
const userServices = new UserService();
const instanceService = new InstanceService();
export default async function HomePage() {
const instanceDetails = await instanceService.getInstanceInfo().catch(() => undefined);
const user = await userServices
.currentUser()
.then((user) => ({ ...user, isAuthenticated: true }))
.catch(() => ({ isAuthenticated: false }));
if (!instanceDetails) {
return <InstanceFailureView />;
}
if (!instanceDetails?.instance?.is_setup_done) {
<InstanceNotReady />;
}
if (user.isAuthenticated) {
return <UserLoggedIn />;
}
return (
<AppProvider initialState={{ instance: instanceDetails.instance }}>
<AuthView />
</AppProvider>
);
}
@@ -1,3 +1,5 @@
"use client";
import React from "react";
import { Controller, useForm } from "react-hook-form";
// icons
@@ -1,3 +1,4 @@
"use client";
import { Fragment, useState } from "react";
import { usePopper } from "react-popper";
import { X } from "lucide-react";
@@ -1,7 +1,9 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
// icons
import Link from "next/link";
import { useRouter } from "next/router";
import { useParams } from "next/navigation";
import { Eye, EyeOff, XCircle } from "lucide-react";
// ui
import { Button, Input, Spinner } from "@plane/ui";
@@ -12,7 +14,7 @@ import { API_BASE_URL } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper";
// hooks
import { useInstance } from "@/hooks/store";
import { AuthService } from "@/services/authentication.service";
import { AuthService } from "@/services/auth.service";
type Props = {
email: string;
@@ -43,12 +45,11 @@ export const PasswordForm: React.FC<Props> = (props) => {
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// hooks
const { instance } = useInstance();
const { data: instance, config: instanceConfig } = useInstance();
// router
const router = useRouter();
const { next_path } = router.query;
const { next_path } = useParams<any>();
// derived values
const isSmtpConfigured = instance?.config?.is_smtp_configured;
const isSmtpConfigured = instanceConfig?.is_smtp_configured;
const handleFormChange = (key: keyof TPasswordFormValues, value: string) =>
setPasswordFormData((prev) => ({ ...prev, [key]: value }));
@@ -1,3 +1,5 @@
"use client";
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
@@ -7,7 +9,7 @@ import { EmailForm, UniqueCodeForm, PasswordForm, OAuthOptions, TermsAndConditio
import { useInstance } from "@/hooks/store";
import useToast from "@/hooks/use-toast";
// services
import { AuthService } from "@/services/authentication.service";
import { AuthService } from "@/services/auth.service";
export enum EAuthSteps {
EMAIL = "EMAIL",
@@ -60,9 +62,9 @@ export const AuthRoot = observer(() => {
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState("");
// hooks
const { instance } = useInstance();
const { config: instanceConfig } = useInstance();
// derived values
const isSmtpConfigured = instance?.config?.is_smtp_configured;
const isSmtpConfigured = instanceConfig?.is_smtp_configured;
const { header, subHeader } = getHeaderSubHeader(authMode);
@@ -112,8 +114,8 @@ export const AuthRoot = observer(() => {
);
};
const isOAuthEnabled =
instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled);
const isOAuthEnabled = instanceConfig && (instanceConfig?.is_google_enabled || instanceConfig?.is_github_enabled);
return (
<div className="relative flex flex-col space-y-6">
<div className="space-y-1 text-center">
@@ -149,7 +151,7 @@ export const AuthRoot = observer(() => {
)}
</>
)}
{isOAuthEnabled && <OAuthOptions />}
{isOAuthEnabled !== undefined && <OAuthOptions />}
<TermsAndConditions mode={authMode} />
</div>
);
@@ -1,5 +1,7 @@
"use client";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { useParams } from "next/navigation";
// icons
import { CircleCheck, XCircle } from "lucide-react";
// ui
@@ -10,7 +12,7 @@ import { API_BASE_URL } from "@/helpers/common.helper";
import useTimer from "@/hooks/use-timer";
import useToast from "@/hooks/use-toast";
// services
import { AuthService } from "@/services/authentication.service";
import { AuthService } from "@/services/auth.service";
// types
import { IEmailCheckData } from "@/types/auth";
import { EAuthModes } from "./root";
@@ -43,8 +45,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isSubmitting, setIsSubmitting] = useState(false);
// router
const router = useRouter();
const { next_path } = router.query;
const { next_path } = useParams<any>();
// toast alert
const { setToastAlert } = useToast();
// timer
@@ -1,3 +1,5 @@
"use client";
import { FC } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
@@ -1,3 +1,5 @@
"use client";
import { FC } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
@@ -1,3 +1,5 @@
"use client";
import { observer } from "mobx-react-lite";
// components
import { GithubOAuthButton, GoogleOAuthButton } from "@/components/accounts";
@@ -6,7 +8,7 @@ import { useInstance } from "@/hooks/store";
export const OAuthOptions: React.FC = observer(() => {
// hooks
const { instance } = useInstance();
const { config: instanceConfig } = useInstance();
return (
<>
@@ -16,12 +18,12 @@ export const OAuthOptions: React.FC = observer(() => {
<hr className="w-full border-onboarding-border-100" />
</div>
<div className={`mx-auto mt-7 grid gap-4 overflow-hidden sm:w-96`}>
{instance?.config?.is_google_enabled && (
{instanceConfig?.is_google_enabled && (
<div className="flex h-[42px] items-center !overflow-hidden">
<GoogleOAuthButton text="SignIn with Google" />
</div>
)}
{instance?.config?.is_github_enabled && <GithubOAuthButton text="SignIn with Github" />}
{instanceConfig?.is_github_enabled && <GithubOAuthButton text="SignIn with Github" />}
</div>
</>
);
@@ -1,3 +1,5 @@
"use client";
import React, { useMemo, useState } from "react";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
@@ -8,7 +10,7 @@ import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { UserImageUploadModal } from "@/components/accounts";
// hooks
import { useMobxStore } from "@/hooks/store";
import { useUser } from "@/hooks/store";
// services
import fileService from "@/services/file.service";
@@ -35,9 +37,7 @@ export const OnBoardingForm: React.FC<Props> = observer((props) => {
const [isRemoving, setIsRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
// store hooks
const {
user: { updateCurrentUser },
} = useMobxStore();
const { updateCurrentUser } = useUser();
// form info
const {
getValues,
@@ -1,3 +1,5 @@
"use client";
// icons
import { CircleCheck } from "lucide-react";
// helpers
@@ -1,3 +1,5 @@
"use client";
import React, { FC } from "react";
import Link from "next/link";
import { EAuthModes } from "./auth-forms";
@@ -1,3 +1,4 @@
"use client";
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import { useDropzone } from "react-dropzone";
@@ -27,7 +28,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
const [image, setImage] = useState<File | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false);
// store hooks
const { instance } = useInstance();
const { config: instanceConfig } = useInstance();
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);
@@ -36,7 +37,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
accept: {
"image/*": [".png", ".jpg", ".jpeg", ".svg", ".webp"],
},
maxSize: instance?.config?.file_size_limit ?? MAX_FILE_SIZE,
maxSize: (instanceConfig?.file_size_limit as number) ?? MAX_FILE_SIZE,
multiple: false,
});
@@ -1,3 +1,5 @@
"use client";
import Image from "next/image";
// hooks
import { useUser } from "@/hooks/store";
+2 -1
View File
@@ -1 +1,2 @@
export * from "./not-ready-view";
export * from "./not-ready-view";
export * from "./instance-failure-view";
@@ -0,0 +1,42 @@
"use client";
import { FC } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Button } from "@plane/ui";
// assets
import InstanceFailureDarkImage from "public/instance/instance-failure-dark.svg";
import InstanceFailureImage from "public/instance/instance-failure.svg";
type InstanceFailureViewProps = {
// mutate: () => void;
};
export const InstanceFailureView: FC<InstanceFailureViewProps> = () => {
const { resolvedTheme } = useTheme();
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
const handleRetry = () => {
window.location.reload();
};
return (
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center mt-10">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<Image src={instanceImage} alt="Plane Logo" />
<h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3>
<p className="font-medium text-base text-center">
We were unable to fetch the details of the instance. <br />
Fret not, it might just be a connectivity issue.
</p>
</div>
<div className="flex justify-center">
<Button size="md" onClick={handleRetry}>
Retry
</Button>
</div>
</div>
</div>
);
};
+22 -29
View File
@@ -1,40 +1,33 @@
"use client";
import { FC } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
// icons
import { UserCog2 } from "lucide-react";
// ui
import { getButtonStyling } from "@plane/ui";
import { Button } from "@plane/ui";
// helper
import { ADMIN_BASE_URL, ADMIN_BASE_PATH } from "@/helpers/common.helper";
// images
import instanceNotReady from "public/instance/plane-instance-not-ready.webp";
import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png";
export const InstanceNotReady: FC = () => {
const { resolvedTheme } = useTheme();
const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo;
const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH);
return (
<div className="h-screen w-full overflow-y-auto bg-onboarding-gradient-100">
<div className="h-full w-full pt-24">
<div className="mx-auto h-full rounded-t-md border-x border-t border-custom-border-100 bg-onboarding-gradient-100 px-4 pt-4 shadow-sm sm:w-4/5 md:w-2/3">
<div className="relative h-full rounded-t-md bg-onboarding-gradient-200 px-7 sm:px-0">
<div className="flex items-center justify-center py-10">
<Image src={planeLogo} className="h-[44px] w-full" alt="Plane logo" />
</div>
<div className="mt-20">
<Image src={instanceNotReady} className="w-full" alt="Instance not ready" />
</div>
<div className="flex w-full flex-col items-center gap-5 py-12 pb-20">
<h3 className="text-2xl font-medium">Your Plane instance isn{"'"}t ready yet</h3>
<p className="text-sm">Ask your Instance Admin to complete set-up first.</p>
<a href="/god-mode" className={`${getButtonStyling("primary", "md")} mt-4`}>
<UserCog2 className="h-3.5 w-3.5" />
Get started
</a>
</div>
</div>
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center pt-12">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-onboarding-text-400">
Get started by setting up your instance and workspace
</p>
</div>
<div>
<a href={GOD_MODE_URL}>
<Button size="lg" className="w-full">
Get started
</Button>
</a>
</div>
</div>
</div>
@@ -1,53 +1,48 @@
"use client";
// mobx react lite
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
// components
import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date";
import { IssueBlockPriority } from "@/components/issues/board-views/block-priority";
import { IssueBlockState } from "@/components/issues/board-views/block-state";
import { useMobxStore } from "@/hooks/store";
// components
// hooks
import { useIssueDetails, useProject } from "@/hooks/store";
// interfaces
import { RootStore } from "@/store/root.store";
import { IIssue } from "types/issue";
import { IIssue } from "@/types/issue";
export const IssueKanBanBlock = observer(({ issue }: { issue: IIssue }) => {
const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
type IssueKanBanBlockProps = {
issue: IIssue;
workspaceSlug: string;
projectId: string;
params: any;
};
export const IssueKanBanBlock: FC<IssueKanBanBlockProps> = observer((props) => {
const { workspaceSlug, projectId, params, issue } = props;
const { board, priorities, states, labels } = params;
// store
const { project } = useProject();
const { setPeekId } = useIssueDetails();
// router
const router = useRouter();
const { workspace_slug, project_slug, board, priorities, states, labels } = router.query as {
workspace_slug: string;
project_slug: string;
board: string;
priorities: string;
states: string;
labels: string;
};
const searchParams = useSearchParams();
const handleBlockClick = () => {
issueDetailStore.setPeekId(issue.id);
setPeekId(issue.id);
const params: any = { board: board, peekId: issue.id };
if (states && states.length > 0) params.states = states;
if (priorities && priorities.length > 0) params.priorities = priorities;
if (labels && labels.length > 0) params.labels = labels;
router.push(
{
pathname: `/${workspace_slug}/${project_slug}`,
query: { ...params },
},
undefined,
{ shallow: true }
);
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
};
return (
<div className="flex flex-col gap-1.5 space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs">
{/* id */}
<div className="break-words text-xs text-custom-text-300">
{projectStore?.project?.identifier}-{issue?.sequence_id}
{project?.identifier}-{issue?.sequence_id}
</div>
{/* name */}
@@ -1,18 +1,16 @@
// mobx react lite
import { observer } from "mobx-react-lite";
// interfaces
// constants
import { StateGroupIcon } from "@plane/ui";
import { issueGroupFilter } from "@/constants/data";
// ui
import { StateGroupIcon } from "@plane/ui";
// constants
import { issueGroupFilter } from "@/constants/data";
// mobx hook
import { useMobxStore } from "@/hooks/store";
import { RootStore } from "@/store/root.store";
import { IIssueState } from "types/issue";
import { useIssue } from "@/hooks/store";
// interfaces
import { IIssueState } from "@/types/issue";
export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) => {
const store: RootStore = useMobxStore();
const { getCountOfIssuesByState } = useIssue();
const stateGroup = issueGroupFilter(state.group);
if (stateGroup === null) return <></>;
@@ -23,9 +21,7 @@ export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) =>
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
</div>
<div className="mr-1 truncate font-semibold capitalize text-custom-text-200">{state?.name}</div>
<span className="flex-shrink-0 rounded-full text-custom-text-300">
{store.issue.getCountOfIssuesByState(state.id)}
</span>
<span className="flex-shrink-0 rounded-full text-custom-text-300">{getCountOfIssuesByState(state.id)}</span>
</div>
);
});
@@ -1,36 +1,47 @@
"use client";
// mobx react lite
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { IssueKanBanBlock } from "@/components/issues/board-views/kanban/block";
import { IssueKanBanHeader } from "@/components/issues/board-views/kanban/header";
// ui
import { Icon } from "@/components/ui";
// interfaces
// mobx hook
import { useMobxStore } from "@/hooks/store";
import { RootStore } from "@/store/root.store";
import { IIssueState, IIssue } from "types/issue";
import { useIssue } from "@/hooks/store";
// interfaces
import { IIssueState, IIssue } from "@/types/issue";
export const IssueKanbanView = observer(() => {
const store: RootStore = useMobxStore();
type IssueKanbanViewProps = {
workspaceSlug: string;
projectId: string;
};
export const IssueKanbanView: FC<IssueKanbanViewProps> = observer((props) => {
const { workspaceSlug, projectId } = props;
// store hooks
const { states, getFilteredIssuesByState } = useIssue();
return (
<div className="relative flex h-full w-full gap-3 overflow-hidden overflow-x-auto">
{store?.issue?.states &&
store?.issue?.states.length > 0 &&
store?.issue?.states.map((_state: IIssueState) => (
{states &&
states.length > 0 &&
states.map((_state: IIssueState) => (
<div key={_state.id} className="relative flex h-full w-[340px] flex-shrink-0 flex-col">
<div className="flex-shrink-0">
<IssueKanBanHeader state={_state} />
</div>
<div className="hide-vertical-scrollbar h-full w-full overflow-hidden overflow-y-auto">
{store.issue.getFilteredIssuesByState(_state.id) &&
store.issue.getFilteredIssuesByState(_state.id).length > 0 ? (
{getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? (
<div className="space-y-3 px-2 pb-2">
{store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
<IssueKanBanBlock key={_issue.id} issue={_issue} />
{getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
<IssueKanBanBlock
key={_issue.id}
issue={_issue}
workspaceSlug={workspaceSlug}
projectId={projectId}
params={{}}
/>
))}
</div>
) : (
@@ -1,47 +1,40 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { useParams, useRouter, useSearchParams } from "next/navigation";
// components
import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date";
import { IssueBlockLabels } from "@/components/issues/board-views/block-labels";
import { IssueBlockPriority } from "@/components/issues/board-views/block-priority";
import { IssueBlockState } from "@/components/issues/board-views/block-state";
// mobx hook
import { useMobxStore } from "@/hooks/store";
import { useIssueDetails, useProject } from "@/hooks/store";
// interfaces
import { RootStore } from "@/store/root.store";
import { IIssue } from "types/issue";
import { IIssue } from "@/types/issue";
// store
export const IssueListBlock: FC<{ issue: IIssue }> = observer((props) => {
const { issue } = props;
type IssueListBlockProps = {
issue: IIssue;
workspaceSlug: string;
projectId: string;
};
export const IssueListBlock: FC<IssueListBlockProps> = observer((props) => {
const { workspaceSlug, projectId, issue } = props;
const { board, states, priorities, labels } = useParams<any>();
const searchParams = useSearchParams();
// store
const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
const { project } = useProject();
const { setPeekId } = useIssueDetails();
// router
const router = useRouter();
const { workspace_slug, project_slug, board, priorities, states, labels } = router.query as {
workspace_slug: string;
project_slug: string;
board: string;
priorities: string;
states: string;
labels: string;
};
const handleBlockClick = () => {
issueDetailStore.setPeekId(issue.id);
setPeekId(issue.id);
const params: any = { board: board, peekId: issue.id };
if (states && states.length > 0) params.states = states;
if (priorities && priorities.length > 0) params.priorities = priorities;
if (labels && labels.length > 0) params.labels = labels;
router.push(
{
pathname: `/${workspace_slug}/${project_slug}`,
query: { ...params },
},
undefined,
{ shallow: true }
);
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
// router.push(`/${workspace_slug?.toString()}/${project_slug}?board=${board?.toString()}&peekId=${issue.id}`);
};
@@ -50,7 +43,7 @@ export const IssueListBlock: FC<{ issue: IIssue }> = observer((props) => {
<div className="relative flex w-full flex-grow items-center gap-3 overflow-hidden">
{/* id */}
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
{projectStore?.project?.identifier}-{issue?.sequence_id}
{project?.identifier}-{issue?.sequence_id}
</div>
{/* name */}
<div onClick={handleBlockClick} className="flex-grow cursor-pointer truncate text-sm font-medium">
@@ -1,18 +1,15 @@
// mobx react lite
import { observer } from "mobx-react-lite";
// interfaces
// ui
import { StateGroupIcon } from "@plane/ui";
// constants
import { issueGroupFilter } from "@/constants/data";
// mobx hook
import { useMobxStore } from "@/hooks/store";
import { RootStore } from "@/store/root.store";
import { IIssueState } from "types/issue";
import { useIssue } from "@/hooks/store";
// types
import { IIssueState } from "@/types/issue";
export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
const store: RootStore = useMobxStore();
const { getCountOfIssuesByState } = useIssue();
const stateGroup = issueGroupFilter(state.group);
if (stateGroup === null) return <></>;
@@ -23,7 +20,7 @@ export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
</div>
<div className="mr-1 font-medium capitalize">{state?.name}</div>
<div className="text-sm font-medium text-custom-text-200">{store.issue.getCountOfIssuesByState(state.id)}</div>
<div className="text-sm font-medium text-custom-text-200">{getCountOfIssuesByState(state.id)}</div>
</div>
);
});
@@ -1,29 +1,34 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { IssueListBlock } from "@/components/issues/board-views/list/block";
import { IssueListHeader } from "@/components/issues/board-views/list/header";
// interfaces
// mobx hook
import { useMobxStore } from "@/hooks/store";
// store
import { RootStore } from "@/store/root.store";
import { IIssueState, IIssue } from "types/issue";
import { useIssue } from "@/hooks/store";
// types
import { IIssueState, IIssue } from "@/types/issue";
export const IssueListView = observer(() => {
const { issue: issueStore }: RootStore = useMobxStore();
type IssueListViewProps = {
workspaceSlug: string;
projectId: string;
};
export const IssueListView: FC<IssueListViewProps> = observer((props) => {
const { workspaceSlug, projectId } = props;
// store hooks
const { states, getFilteredIssuesByState } = useIssue();
return (
<>
{issueStore?.states &&
issueStore?.states.length > 0 &&
issueStore?.states.map((_state: IIssueState) => (
{states &&
states.length > 0 &&
states.map((_state: IIssueState) => (
<div key={_state.id} className="relative w-full">
<IssueListHeader state={_state} />
{issueStore.getFilteredIssuesByState(_state.id) &&
issueStore.getFilteredIssuesByState(_state.id).length > 0 ? (
{getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? (
<div className="divide-y divide-custom-border-200">
{issueStore.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
<IssueListBlock key={_issue.id} issue={_issue} />
{getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
<IssueListBlock key={_issue.id} issue={_issue} workspaceSlug={workspaceSlug} projectId={projectId} />
))}
</div>
) : (
@@ -1,12 +1,10 @@
// components
// icons
import { X } from "lucide-react";
// helpers
import { IIssueFilterOptions } from "@/store/issues/types";
import { IIssueLabel, IIssueState } from "types/issue";
// types
import { IIssueLabel, IIssueState, IIssueFilterOptions } from "@/types/issue";
// components
import { AppliedPriorityFilters } from "./priority";
import { AppliedStateFilters } from "./state";
// types
type Props = {
appliedFilters: IIssueFilterOptions;
@@ -1,33 +1,31 @@
"use client";
import { FC, useCallback } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// components
import { useRouter } from "next/navigation";
// hooks
import { useIssue, useProject, useIssueFilter } from "@/hooks/store";
// store
import { useMobxStore } from "@/hooks/store";
import { IIssueFilterOptions } from "@/store/issues/types";
import { RootStore } from "@/store/root.store";
import { IIssueFilterOptions } from "@/types/issue";
// components
import { AppliedFiltersList } from "./filters-list";
export const IssueAppliedFilters: FC = observer(() => {
// TODO: fix component types
export const IssueAppliedFilters: FC = observer((props: any) => {
const router = useRouter();
const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as {
workspace_slug: string;
project_slug: string;
};
const {
issuesFilter: { issueFilters, updateFilters },
issue: { states, labels },
project: { activeBoard },
}: RootStore = useMobxStore();
const { workspaceSlug, projectId } = props;
const { states, labels } = useIssue();
const { activeLayout } = useProject();
const { issueFilters, updateFilters } = useIssueFilter();
const userFilters = issueFilters?.filters || {};
const appliedFilters: IIssueFilterOptions = {};
const appliedFilters: any = {};
Object.entries(userFilters).forEach(([key, value]) => {
if (!value) return;
if (Array.isArray(value) && value.length === 0) return;
appliedFilters[key as keyof IIssueFilterOptions] = value;
appliedFilters[key] = value;
});
const updateRouteParams = useCallback(
@@ -36,16 +34,17 @@ export const IssueAppliedFilters: FC = observer(() => {
const priority = key === "priority" ? value || [] : issueFilters?.filters?.priority ?? [];
const labels = key === "labels" ? value || [] : issueFilters?.filters?.labels ?? [];
let params: any = { board: activeBoard || "list" };
let params: any = { board: activeLayout || "list" };
if (!clearFields) {
if (priority.length > 0) params = { ...params, priorities: priority.join(",") };
if (state.length > 0) params = { ...params, states: state.join(",") };
if (labels.length > 0) params = { ...params, labels: labels.join(",") };
}
router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true });
console.log("params", params);
// TODO: fix this redirection
// router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true });
},
[workspaceSlug, projectId, activeBoard, issueFilters, router]
[workspaceSlug, projectId, activeLayout, issueFilters, router]
);
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
@@ -80,7 +79,7 @@ export const IssueAppliedFilters: FC = observer(() => {
<div className="border-b border-custom-border-200 p-5 py-3">
<AppliedFiltersList
appliedFilters={appliedFilters || {}}
handleRemoveFilter={handleRemoveFilter}
handleRemoveFilter={handleRemoveFilter as any}
handleRemoveAllFilters={handleRemoveAllFilters}
labels={labels ?? []}
states={states ?? []}
+25 -25
View File
@@ -1,29 +1,29 @@
import { FC, useCallback } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// hooks
import { useIssue, useIssueFilter, useProject } from "@/hooks/store";
// types
import { IIssueFilterOptions } from "@/types/issue";
// components
import { useMobxStore } from "@/hooks/store";
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/store/issues/helpers";
import { IIssueFilterOptions } from "@/store/issues/types";
import { RootStore } from "@/store/root.store";
import { FiltersDropdown } from "./helpers/dropdown";
import { FilterSelection } from "./selection";
// types
// helpers
// store
export const IssueFiltersDropdown: FC = observer(() => {
type IssueFiltersDropdownProps = {
workspaceSlug: string;
projectId: string;
};
export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((props) => {
const { workspaceSlug, projectId } = props;
const searchParams = useSearchParams();
const router = useRouter();
const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as {
workspace_slug: string;
project_slug: string;
};
const {
project: { activeBoard },
issue: { states, labels },
issuesFilter: { issueFilters, updateFilters },
}: RootStore = useMobxStore();
// store hooks
const { activeLayout } = useProject();
const { states, labels } = useIssue();
const { issueFilters, updateFilters } = useIssueFilter();
const updateRouteParams = useCallback(
(key: keyof IIssueFilterOptions, value: string[]) => {
@@ -31,14 +31,14 @@ export const IssueFiltersDropdown: FC = observer(() => {
const priority = key === "priority" ? value : issueFilters?.filters?.priority ?? [];
const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? [];
let params: any = { board: activeBoard || "list" };
let params: any = { board: activeLayout || "list" };
if (priority.length > 0) params = { ...params, priorities: priority.join(",") };
if (state.length > 0) params = { ...params, states: state.join(",") };
if (labels.length > 0) params = { ...params, labels: labels.join(",") };
router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true });
console.log("params", params);
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
},
[workspaceSlug, projectId, activeBoard, issueFilters, router]
[workspaceSlug, projectId, activeLayout, issueFilters, router]
);
const handleFilters = useCallback(
@@ -66,8 +66,8 @@ export const IssueFiltersDropdown: FC = observer(() => {
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFilters={handleFilters}
layoutDisplayFiltersOptions={activeBoard ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeBoard] : undefined}
handleFilters={handleFilters as any}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
states={states ?? undefined}
labels={labels ?? undefined}
/>
@@ -1,13 +1,10 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import { Search, X } from "lucide-react";
// components
// types
// filter helpers
import { ILayoutDisplayFiltersOptions } from "@/store/issues/helpers";
import { IIssueFilterOptions } from "@/store/issues/types";
import { IIssueState, IIssueLabel } from "types/issue";
import { IIssueState, IIssueLabel, IIssueFilterOptions } from "@/types/issue";
import { ILayoutDisplayFiltersOptions } from "@/types/issue-filters";
// components
import { FilterPriority, FilterState } from "./";
type Props = {
+50 -57
View File
@@ -1,54 +1,52 @@
import { useEffect } from "react";
"use client";
import { useEffect, FC } from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router";
// components
import { useRouter, useParams, useSearchParams, usePathname } from "next/navigation";
import { Briefcase } from "lucide-react";
import { Avatar, Button } from "@plane/ui";
// components
import { ProjectLogo } from "@/components/common";
import { IssueFiltersDropdown } from "@/components/issues/filters";
// hooks
import { useMobxStore, useUser } from "@/hooks/store";
// store
import { RootStore } from "@/store/root.store";
import { useProject, useUser, useIssueFilter } from "@/hooks/store";
// types
import { TIssueBoardKeys } from "@/types/issue";
// components
import { NavbarIssueBoardView } from "./issue-board-view";
import { NavbarTheme } from "./theme";
const IssueNavbar = observer(() => {
const {
project: projectStore,
issuesFilter: { updateFilters },
}: RootStore = useMobxStore();
const { data: user } = useUser();
// router
type IssueNavbarProps = {
projectSettings: any;
workspaceSlug: string;
projectId: string;
};
const IssueNavbar: FC<IssueNavbarProps> = observer((props) => {
const { projectSettings, workspaceSlug, projectId } = props;
const { project_details, views } = projectSettings;
const { board, labels, states, priorities, peekId } = useParams<any>();
const searchParams = useSearchParams();
const pathName = usePathname();
// hooks
const router = useRouter();
const { workspace_slug, project_slug, board, peekId, states, priorities, labels } = router.query as {
workspace_slug: string;
project_slug: string;
peekId: string;
board: string;
states: string;
priorities: string;
labels: string;
};
// store
const { settings, activeLayout, hydrate, setActiveLayout } = useProject();
const { data: user } = useUser();
const { updateFilters } = useIssueFilter();
hydrate(projectSettings);
useEffect(() => {
if (workspace_slug && project_slug) {
projectStore.fetchProjectSettings(workspace_slug.toString(), project_slug.toString());
}
}, [projectStore, workspace_slug, project_slug]);
useEffect(() => {
if (workspace_slug && project_slug && projectStore?.deploySettings) {
if (workspaceSlug && projectId && settings) {
const viewsAcceptable: string[] = [];
let currentBoard: TIssueBoardKeys | null = null;
if (projectStore?.deploySettings?.views?.list) viewsAcceptable.push("list");
if (projectStore?.deploySettings?.views?.kanban) viewsAcceptable.push("kanban");
if (projectStore?.deploySettings?.views?.calendar) viewsAcceptable.push("calendar");
if (projectStore?.deploySettings?.views?.gantt) viewsAcceptable.push("gantt");
if (projectStore?.deploySettings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet");
if (settings?.views?.list) viewsAcceptable.push("list");
if (settings?.views?.kanban) viewsAcceptable.push("kanban");
if (settings?.views?.calendar) viewsAcceptable.push("calendar");
if (settings?.views?.gantt) viewsAcceptable.push("gantt");
if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet");
if (board) {
if (viewsAcceptable.includes(board.toString())) {
@@ -65,49 +63,47 @@ const IssueNavbar = observer(() => {
}
if (currentBoard) {
if (projectStore?.activeBoard === null || projectStore?.activeBoard !== currentBoard) {
if (activeLayout === null || activeLayout !== currentBoard) {
let params: any = { board: currentBoard };
if (peekId && peekId.length > 0) params = { ...params, peekId: peekId };
if (priorities && priorities.length > 0) params = { ...params, priorities: priorities };
if (states && states.length > 0) params = { ...params, states: states };
if (labels && labels.length > 0) params = { ...params, labels: labels };
console.log("params", params);
let storeParams: any = {};
if (priorities && priorities.length > 0) storeParams = { ...storeParams, priority: priorities.split(",") };
if (states && states.length > 0) storeParams = { ...storeParams, state: states.split(",") };
if (labels && labels.length > 0) storeParams = { ...storeParams, labels: labels.split(",") };
if (storeParams) updateFilters(project_slug, storeParams);
projectStore.setActiveBoard(currentBoard);
router.push({
pathname: `/${workspace_slug}/${project_slug}`,
query: { ...params },
});
if (storeParams) updateFilters(projectId, storeParams);
setActiveLayout(currentBoard);
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
}
}
}
}, [
board,
workspace_slug,
project_slug,
workspaceSlug,
projectId,
router,
projectStore,
projectStore?.deploySettings,
updateFilters,
labels,
states,
priorities,
peekId,
settings,
activeLayout,
setActiveLayout,
searchParams,
]);
return (
<div className="relative flex w-full items-center gap-4 px-5">
{/* project detail */}
<div className="flex flex-shrink-0 items-center gap-2">
{projectStore.project ? (
{project_details ? (
<span className="h-7 w-7 flex-shrink-0 grid place-items-center">
<ProjectLogo logo={projectStore.project.logo_props} className="text-lg" />
<ProjectLogo logo={project_details.logo_props} className="text-lg" />
</span>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
@@ -115,21 +111,18 @@ const IssueNavbar = observer(() => {
</span>
)}
<div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium">
{projectStore?.project?.name || `...`}
{project_details?.name || `...`}
</div>
</div>
{/* issue search bar */}
<div className="w-full">{/* <NavbarSearch /> */}</div>
{/* issue views */}
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
<NavbarIssueBoardView />
<NavbarIssueBoardView layouts={views} />
</div>
{/* issue filters */}
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
<IssueFiltersDropdown />
<IssueFiltersDropdown workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
{/* theming */}
@@ -137,14 +130,14 @@ const IssueNavbar = observer(() => {
<NavbarTheme />
</div>
{user ? (
{user?.id ? (
<div className="flex items-center gap-2 rounded border border-custom-border-200 p-2">
<Avatar name={user?.display_name} src={user?.avatar ?? undefined} shape="square" size="sm" />
<h6 className="text-xs font-medium">{user.display_name}</h6>
</div>
) : (
<div className="flex-shrink-0">
<Link href={`/?next_path=${router.asPath}`}>
<Link href={`/?next_path=${pathName}`}>
<Button variant="outline-primary">Sign in</Button>
</Link>
</div>
@@ -1,47 +1,49 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// constants
import { issueViews } from "@/constants/data";
// hooks
import { useProject } from "@/hooks/store";
// mobx
import { useMobxStore } from "@/hooks/store";
import { RootStore } from "@/store/root.store";
import { TIssueBoardKeys } from "types/issue";
import { TIssueBoardKeys } from "@/types/issue";
export const NavbarIssueBoardView = observer(() => {
const {
project: { viewOptions, setActiveBoard, activeBoard },
}: RootStore = useMobxStore();
// router
const router = useRouter();
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
type NavbarIssueBoardViewProps = {
layouts: Record<TIssueBoardKeys, boolean>;
};
export const NavbarIssueBoardView: FC<NavbarIssueBoardViewProps> = observer((props) => {
const { layouts } = props;
const { activeLayout, setActiveLayout } = useProject();
const handleCurrentBoardView = (boardView: string) => {
setActiveBoard(boardView as TIssueBoardKeys);
router.push(`/${workspace_slug}/${project_slug}?board=${boardView}`);
setActiveLayout(boardView as TIssueBoardKeys);
};
return (
<>
{viewOptions &&
Object.keys(viewOptions).map((viewKey: string) => {
if (viewOptions[viewKey]) {
{layouts &&
Object.keys(layouts).map((layoutKey: string) => {
if (layouts[layoutKey as TIssueBoardKeys]) {
return (
<div
key={viewKey}
key={layoutKey}
className={`flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded-sm ${
viewKey === activeBoard
layoutKey === activeLayout
? `bg-custom-background-80 text-custom-text-200`
: `text-custom-text-300 hover:bg-custom-background-80`
}`}
onClick={() => handleCurrentBoardView(viewKey)}
title={viewKey}
onClick={() => handleCurrentBoardView(layoutKey)}
title={layoutKey}
>
<span
className={`material-symbols-rounded text-[18px] ${
issueViews[viewKey]?.className ? issueViews[viewKey]?.className : ``
issueViews[layoutKey]?.className ? issueViews[layoutKey]?.className : ``
}`}
>
{issueViews[viewKey]?.icon}
{issueViews[layoutKey]?.icon}
</span>
</div>
);
+2 -1
View File
@@ -1,3 +1,5 @@
"use client";
// next theme
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
@@ -16,7 +18,6 @@ export const NavbarTheme = observer(() => {
useEffect(() => {
if (!theme) return;
setAppTheme(theme);
}, [theme]);
@@ -1,12 +1,11 @@
import React, { useRef } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { useForm, Controller } from "react-hook-form";
// components
import { EditorRefApi } from "@plane/lite-text-editor";
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
// hooks
import { useMobxStore, useUser } from "@/hooks/store";
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
import useToast from "@/hooks/use-toast";
// types
import { Comment } from "@/types/issue";
@@ -17,22 +16,21 @@ const defaultValues: Partial<Comment> = {
type Props = {
disabled?: boolean;
workspaceSlug: string;
projectId: string;
};
export const AddComment: React.FC<Props> = observer(() => {
export const AddComment: React.FC<Props> = observer((props) => {
// const { disabled = false } = props;
const { workspaceSlug, projectId } = props;
// refs
const editorRef = useRef<EditorRefApi>(null);
// router
const router = useRouter();
const { workspace_slug, project_slug } = router.query;
// store hooks
const { project } = useMobxStore();
const { issueDetails: issueDetailStore } = useMobxStore();
const { workspace } = useProject();
const { peekId: issueId, addIssueComment } = useIssueDetails();
const { data: currentUser } = useUser();
// derived values
const workspaceId = project.workspace?.id;
const issueId = issueDetailStore.peekId;
const workspaceId = workspace?.id;
// form info
const {
handleSubmit,
@@ -45,10 +43,9 @@ export const AddComment: React.FC<Props> = observer(() => {
const { setToastAlert } = useToast();
const onSubmit = async (formData: Comment) => {
if (!workspace_slug || !project_slug || !issueId || isSubmitting || !formData.comment_html) return;
if (!workspaceSlug || !projectId || !issueId || isSubmitting || !formData.comment_html) return;
await issueDetailStore
.addIssueComment(workspace_slug.toString(), project_slug.toString(), issueId, formData)
await addIssueComment(workspaceSlug, projectId, issueId, formData)
.then(() => {
reset(defaultValues);
editorRef.current?.clearEditor();
@@ -75,7 +72,7 @@ export const AddComment: React.FC<Props> = observer(() => {
if (currentUser) handleSubmit(onSubmit)(e);
}}
workspaceId={workspaceId as string}
workspaceSlug={workspace_slug as string}
workspaceSlug={workspaceSlug}
ref={editorRef}
initialValue={
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
@@ -10,9 +10,7 @@ import { CommentReactions } from "@/components/issues/peek-overview";
// helpers
import { timeAgo } from "@/helpers/date-time.helper";
// hooks
import { useMobxStore, useUser } from "@/hooks/store";
// store
import { RootStore } from "@/store/root.store";
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
// types
import { Comment } from "@/types/issue";
@@ -23,12 +21,13 @@ type Props = {
export const CommentCard: React.FC<Props> = observer((props) => {
const { comment, workspaceSlug } = props;
const { project }: RootStore = useMobxStore();
const workspaceId = project.workspace?.id;
// store
const { issueDetails: issueDetailStore } = useMobxStore();
// store hooks
const { workspace } = useProject();
const { peekId, deleteIssueComment, updateIssueComment } = useIssueDetails();
const { data: currentUser } = useUser();
// derived values
const workspaceId = workspace?.id;
// states
const [isEditing, setIsEditing] = useState(false);
// refs
@@ -44,15 +43,14 @@ export const CommentCard: React.FC<Props> = observer((props) => {
});
const handleDelete = () => {
if (!workspaceSlug || !issueDetailStore.peekId) return;
issueDetailStore.deleteIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id);
if (!workspaceSlug || !peekId) return;
deleteIssueComment(workspaceSlug, comment.project, peekId, comment.id);
};
const handleCommentUpdate = async (formData: Comment) => {
if (!workspaceSlug || !issueDetailStore.peekId) return;
issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData);
if (!workspaceSlug || !peekId) return;
updateIssueComment(workspaceSlug, comment.project, peekId, comment.id, formData);
setIsEditing(false);
editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html);
};
@@ -135,7 +133,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
</form>
<div className={`${isEditing ? "hidden" : ""}`}>
<LiteTextReadOnlyEditor ref={showEditorRef} initialValue={comment.comment_html} />
<CommentReactions commentId={comment.id} projectId={comment.project} />
<CommentReactions commentId={comment.id} projectId={comment.project} workspaceSlug={workspaceSlug} />
</div>
</div>
</div>
@@ -1,58 +1,38 @@
import React from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { Tooltip } from "@plane/ui";
// ui
import { ReactionSelector } from "@/components/ui";
// helpers
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
// hooks
import { useMobxStore, useUser } from "@/hooks/store";
import { useIssueDetails, useUser } from "@/hooks/store";
type Props = {
commentId: string;
projectId: string;
workspaceSlug: string;
};
export const CommentReactions: React.FC<Props> = observer((props) => {
const { commentId, projectId } = props;
const router = useRouter();
const { workspace_slug } = router.query;
const { commentId, projectId, workspaceSlug } = props;
// hooks
const { issueDetails: issueDetailsStore } = useMobxStore();
const { addCommentReaction, removeCommentReaction, details, peekId } = useIssueDetails();
const { data: user } = useUser();
const peekId = issueDetailsStore.peekId;
const commentReactions = peekId
? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions
: [];
const commentReactions = peekId ? details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions : [];
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
const userReactions = commentReactions?.filter((r) => r.actor_detail.id === user?.id);
const handleAddReaction = (reactionHex: string) => {
if (!workspace_slug || !projectId || !peekId) return;
issueDetailsStore.addCommentReaction(
workspace_slug.toString(),
projectId.toString(),
peekId,
commentId,
reactionHex
);
if (!workspaceSlug || !projectId || !peekId) return;
addCommentReaction(workspaceSlug, projectId, peekId, commentId, reactionHex);
};
const handleRemoveReaction = (reactionHex: string) => {
if (!workspace_slug || !projectId || !peekId) return;
issueDetailsStore.removeCommentReaction(
workspace_slug.toString(),
projectId.toString(),
peekId,
commentId,
reactionHex
);
if (!workspaceSlug || !projectId || !peekId) return;
removeCommentReaction(workspaceSlug, projectId, peekId, commentId, reactionHex);
};
const handleReactionClick = (reactionHex: string) => {
@@ -13,11 +13,12 @@ import { IIssue } from "@/types/issue";
type Props = {
handleClose: () => void;
issueDetails: IIssue | undefined;
workspace_slug: string;
workspaceSlug: string;
projectId: string;
};
export const FullScreenPeekView: React.FC<Props> = observer((props) => {
const { handleClose, issueDetails } = props;
const { handleClose, issueDetails, workspaceSlug, projectId } = props;
return (
<div className="grid h-full w-full grid-cols-10 divide-x divide-custom-border-200 overflow-hidden">
@@ -35,7 +36,11 @@ export const FullScreenPeekView: React.FC<Props> = observer((props) => {
<div className="my-5 h-[1] w-full border-t border-custom-border-200" />
{/* issue activity/comments */}
<div className="w-full pb-5">
<PeekOverviewIssueActivity issueDetails={issueDetails} />
<PeekOverviewIssueActivity
issueDetails={issueDetails}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
</div>
</div>
) : (
@@ -2,19 +2,17 @@ import React from "react";
import { observer } from "mobx-react-lite";
import { MoveRight } from "lucide-react";
import { Listbox, Transition } from "@headlessui/react";
// hooks
// ui
import { Icon } from "@/components/ui";
// helpers
import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks
import { useIssueDetails } from "@/hooks/store";
import useToast from "@/hooks/use-toast";
// store
import { useMobxStore } from "@/hooks/store";
import { IPeekMode } from "@/store/issue_details";
import { RootStore } from "@/store/root.store";
// lib
import useToast from "hooks/use-toast";
import { IPeekMode } from "@/store/issue-detail.store";
// types
import { IIssue } from "types/issue";
import { IIssue } from "@/types/issue";
type Props = {
handleClose: () => void;
@@ -42,7 +40,7 @@ const peekModes: {
export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
const { handleClose } = props;
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
const { peekMode, setPeekMode } = useIssueDetails();
const { setToastAlert } = useToast();
@@ -62,21 +60,19 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
<>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
{issueDetailStore.peekMode === "side" && (
{peekMode === "side" && (
<button type="button" onClick={handleClose}>
<MoveRight className="h-3.5 w-3.5" strokeWidth={2} />
</button>
)}
<Listbox
as="div"
value={issueDetailStore.peekMode}
onChange={(val) => issueDetailStore.setPeekMode(val)}
value={peekMode}
onChange={(val) => setPeekMode(val)}
className="relative flex-shrink-0 text-left"
>
<Listbox.Button
className={`grid place-items-center ${issueDetailStore.peekMode === "full" ? "rotate-45" : ""}`}
>
<Icon iconName={peekModes.find((m) => m.key === issueDetailStore.peekMode)?.icon ?? ""} />
<Listbox.Button className={`grid place-items-center ${peekMode === "full" ? "rotate-45" : ""}`}>
<Icon iconName={peekModes.find((m) => m.key === peekMode)?.icon ?? ""} />
</Listbox.Button>
<Transition
@@ -121,7 +117,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
</Transition>
</Listbox>
</div>
{(issueDetailStore.peekMode === "side" || issueDetailStore.peekMode === "modal") && (
{(peekMode === "side" || peekMode === "modal") && (
<div className="flex flex-shrink-0 items-center gap-2">
<button type="button" onClick={handleCopyLink} className="-rotate-45 focus:outline-none" tabIndex={1}>
<Icon iconName="link" />
@@ -1,44 +1,48 @@
import React from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import { Button } from "@plane/ui";
// components
import { CommentCard, AddComment } from "@/components/issues/peek-overview";
import { Icon } from "@/components/ui";
// hooks
import { useMobxStore, useUser } from "@/hooks/store";
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
// types
import { IIssue } from "@/types/issue";
type Props = {
issueDetails: IIssue;
workspaceSlug: string;
projectId: string;
};
export const PeekOverviewIssueActivity: React.FC<Props> = observer(() => {
export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = props;
// router
const router = useRouter();
const { workspace_slug } = router.query;
const pathname = usePathname();
// store
const { issueDetails: issueDetailStore, project: projectStore } = useMobxStore();
const { canComment } = useProject();
const { details, peekId } = useIssueDetails();
const { data: currentUser } = useUser();
const comments = issueDetailStore.details[issueDetailStore.peekId || ""]?.comments || [];
const comments = details[peekId || ""]?.comments || [];
return (
<div className="pb-10">
<h4 className="font-medium">Activity</h4>
{workspace_slug && (
{workspaceSlug && (
<div className="mt-4">
<div className="space-y-4">
{comments.map((comment: any) => (
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspace_slug?.toString()} />
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspaceSlug?.toString()} />
))}
</div>
{currentUser ? (
<>
{projectStore.deploySettings?.comments && (
{canComment && (
<div className="mt-4">
<AddComment disabled={!currentUser} />
<AddComment disabled={!currentUser} workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
)}
</>
@@ -48,7 +52,7 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer(() => {
<Icon iconName="lock" className="!text-sm" />
Sign in to add your comment
</p>
<Link href={`/?next_path=${router.asPath}`}>
<Link href={`/?next_path=${pathname}`}>
<Button variant="primary">Sign in</Button>
</Link>
</div>
@@ -1,20 +1,22 @@
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// lib
import { Tooltip } from "@plane/ui";
import { ReactionSelector } from "@/components/ui";
// helpers
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
// hooks
import { useMobxStore, useUser } from "@/hooks/store";
import { useIssueDetails, useUser } from "@/hooks/store";
export const IssueEmojiReactions: React.FC = observer(() => {
// router
const router = useRouter();
const { workspace_slug, project_slug } = router.query;
type IssueEmojiReactionsProps = {
workspaceSlug: string;
projectId: string;
};
export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer((props) => {
const { workspaceSlug, projectId } = props;
// store
const { issueDetails: issueDetailsStore } = useMobxStore();
const issueDetailsStore = useIssueDetails();
const { data: user, fetchCurrentUser } = useUser();
const issueId = issueDetailsStore.peekId;
@@ -24,20 +26,17 @@ export const IssueEmojiReactions: React.FC = observer(() => {
const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
const handleAddReaction = (reactionHex: string) => {
if (!workspace_slug || !project_slug || !issueId) return;
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
if (!workspaceSlug || !projectId || !issueId) return;
issueDetailsStore.addIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex);
};
const handleRemoveReaction = (reactionHex: string) => {
if (!workspace_slug || !project_slug || !issueId) return;
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
if (!workspaceSlug || !projectId || !issueId) return;
issueDetailsStore.removeIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex);
};
const handleReactionClick = (reactionHex: string) => {
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
if (userReaction) handleRemoveReaction(reactionHex);
else handleAddReaction(reactionHex);
};
@@ -8,7 +8,7 @@ import { issueGroupFilter, issuePriorityFilter } from "@/constants/data";
import { renderFullDate } from "@/helpers/date-time.helper";
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
// types
import { IPeekMode } from "@/store/issue_details";
import { IPeekMode } from "@/store/issue-detail.store";
// constants
import useToast from "hooks/use-toast";
import { IIssue } from "types/issue";
@@ -1,12 +1,20 @@
import { useParams } from "next/navigation";
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview";
import { useMobxStore } from "@/hooks/store";
import { useProject } from "@/hooks/store";
// type IssueReactionsProps = {
// workspaceSlug: string;
// projectId: string;
// };
export const IssueReactions: React.FC = () => {
const { project: projectStore } = useMobxStore();
const { workspace_slug: workspaceSlug, project_id: projectId } = useParams<any>();
const { canVote, canReact } = useProject();
return (
<div className="mt-4 flex items-center gap-3">
{projectStore?.deploySettings?.votes && (
{canVote && (
<>
<div className="flex items-center gap-2">
<IssueVotes />
@@ -14,9 +22,9 @@ export const IssueReactions: React.FC = () => {
<div className="h-8 w-0.5 bg-custom-background-200" />
</>
)}
{projectStore?.deploySettings?.reactions && (
{canReact && (
<div className="flex items-center gap-2">
<IssueEmojiReactions />
<IssueEmojiReactions workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
)}
</div>
@@ -1,18 +1,17 @@
"use client";
import { useState, useEffect } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { Tooltip } from "@plane/ui";
// hooks
import { useMobxStore, useUser } from "@/hooks/store";
import { useIssueDetails, useUser } from "@/hooks/store";
export const IssueVotes: React.FC = observer(() => {
export const IssueVotes: React.FC = observer((props: any) => {
const { workspaceSlug, projectId } = props;
// states
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const { workspace_slug, project_slug } = router.query;
const { issueDetails: issueDetailsStore } = useMobxStore();
const issueDetailsStore = useIssueDetails();
const { data: user, fetchCurrentUser } = useUser();
const issueId = issueDetailsStore.peekId;
@@ -26,16 +25,16 @@ export const IssueVotes: React.FC = observer(() => {
const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id);
const handleVote = async (e: any, voteValue: 1 | -1) => {
if (!workspace_slug || !project_slug || !issueId) return;
if (!workspaceSlug || !projectId || !issueId) return;
setIsSubmitting(true);
const actionPerformed = votes?.find((vote) => vote.actor === user?.id && vote.vote === voteValue);
if (actionPerformed)
await issueDetailsStore.removeIssueVote(workspace_slug.toString(), project_slug.toString(), issueId);
await issueDetailsStore.removeIssueVote(workspaceSlug.toString(), projectId.toString(), issueId);
else
await issueDetailsStore.addIssueVote(workspace_slug.toString(), project_slug.toString(), issueId, {
await issueDetailsStore.addIssueVote(workspaceSlug.toString(), projectId.toString(), issueId, {
vote: voteValue,
});
@@ -1,42 +1,32 @@
"use client";
import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// mobx
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// components
import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overview";
// lib
import { useMobxStore } from "@/hooks/store";
// store
import { useIssue, useIssueDetails } from "@/hooks/store";
export const IssuePeekOverview: React.FC = observer(() => {
export const IssuePeekOverview: React.FC = observer((props: any) => {
const { workspaceSlug, projectId, peekId, board, priorities, states, labels } = props;
// states
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
// router
const router = useRouter();
const { workspace_slug, project_slug, peekId, board, priorities, states, labels } = router.query as {
workspace_slug: string;
project_slug: string;
peekId: string;
board: string;
priorities: string;
states: string;
labels: string;
};
// store
const { issueDetails: issueDetailStore, issue: issueStore } = useMobxStore();
const issueDetailStore = useIssueDetails();
const issueStore = useIssue();
const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined;
useEffect(() => {
if (workspace_slug && project_slug && peekId && issueStore.issues && issueStore.issues.length > 0) {
if (workspaceSlug && projectId && peekId && issueStore.issues && issueStore.issues.length > 0) {
if (!issueDetails) {
issueDetailStore.fetchIssueDetails(workspace_slug.toString(), project_slug.toString(), peekId.toString());
issueDetailStore.fetchIssueDetails(workspaceSlug.toString(), projectId.toString(), peekId.toString());
}
}
}, [workspace_slug, project_slug, issueDetailStore, issueDetails, peekId, issueStore.issues]);
}, [workspaceSlug, projectId, issueDetailStore, issueDetails, peekId, issueStore.issues]);
const handleClose = () => {
issueDetailStore.setPeekId(null);
@@ -45,10 +35,8 @@ export const IssuePeekOverview: React.FC = observer(() => {
if (states && states.length > 0) params.states = states;
if (priorities && priorities.length > 0) params.priorities = priorities;
if (labels && labels.length > 0) params.labels = labels;
router.replace({ pathname: `/${workspace_slug?.toString()}/${project_slug}`, query: { ...params } }, undefined, {
shallow: true,
});
// TODO: fix this redirection
// router.push( encodeURI(`/${workspaceSlug?.toString()}/${projectId}`, ) { pathname: `/${workspaceSlug?.toString()}/${projectId}`, query: { ...params } });
};
useEffect(() => {
@@ -80,7 +68,12 @@ export const IssuePeekOverview: React.FC = observer(() => {
leaveTo="translate-x-full"
>
<Dialog.Panel className="fixed right-0 top-0 z-20 h-full w-1/2 bg-custom-background-100 shadow-custom-shadow-sm">
<SidePeekView handleClose={handleClose} issueDetails={issueDetails} />
<SidePeekView
handleClose={handleClose}
issueDetails={issueDetails}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
</Dialog.Panel>
</Transition.Child>
</Dialog>
@@ -114,13 +107,19 @@ export const IssuePeekOverview: React.FC = observer(() => {
}`}
>
{issueDetailStore.peekMode === "modal" && (
<SidePeekView handleClose={handleClose} issueDetails={issueDetails} />
<SidePeekView
handleClose={handleClose}
issueDetails={issueDetails}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
)}
{issueDetailStore.peekMode === "full" && (
<FullScreenPeekView
workspace_slug={workspace_slug}
handleClose={handleClose}
issueDetails={issueDetails}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
)}
</div>
@@ -7,16 +7,18 @@ import {
PeekOverviewIssueDetails,
PeekOverviewIssueProperties,
} from "@/components/issues/peek-overview";
import { IIssue } from "types/issue";
// types
import { IIssue } from "@/types/issue";
type Props = {
handleClose: () => void;
issueDetails: IIssue | undefined;
workspaceSlug: string;
projectId: string;
};
export const SidePeekView: React.FC<Props> = observer((props) => {
const { handleClose, issueDetails } = props;
const { handleClose, issueDetails, workspaceSlug, projectId } = props;
return (
<div className="flex h-full w-full flex-col overflow-hidden">
@@ -37,7 +39,11 @@ export const SidePeekView: React.FC<Props> = observer((props) => {
<div className="my-5 h-[1] w-full border-t border-custom-border-200" />
{/* issue activity/comments */}
<div className="w-full pb-5">
<PeekOverviewIssueActivity issueDetails={issueDetails} />
<PeekOverviewIssueActivity
issueDetails={issueDetails}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
</div>
</div>
) : (
+24 -23
View File
@@ -1,3 +1,5 @@
"use client";
import { observer } from "mobx-react-lite";
import Image from "next/image";
// ui
@@ -5,7 +7,7 @@ import { useTheme } from "next-themes";
import useSWR from "swr";
import { Spinner } from "@plane/ui";
// components
import { AuthRoot, UserLoggedIn } from "@/components/accounts";
import { AuthRoot } from "@/components/accounts";
// hooks
import { useUser } from "@/hooks/store";
// images
@@ -17,12 +19,15 @@ export const AuthView = observer(() => {
// hooks
const { resolvedTheme } = useTheme();
// store
const { data: currentUser, fetchCurrentUser, isLoading } = useUser();
const { fetchCurrentUser, isLoading } = useUser();
// fetching user information
const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
shouldRetryOnError: false,
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: true,
errorRetryCount: 1,
});
return (
@@ -33,30 +38,26 @@ export const AuthView = observer(() => {
</div>
) : (
<>
{currentUser ? (
<UserLoggedIn />
) : (
<div className="relative w-screen h-screen overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
<div className="relative w-screen h-screen overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div>
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
<AuthRoot />
</div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10">
<AuthRoot />
</div>
</div>
)}
</div>
</>
)}
</>
+37 -30
View File
@@ -1,7 +1,10 @@
import { useEffect } from "react";
"use client";
import { FC, useEffect } from "react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useRouter } from "next/router";
import { useParams } from "next/navigation";
import useSWR from "swr";
// components
import { IssueCalendarView } from "@/components/issues/board-views/calendar";
import { IssueGanttView } from "@/components/issues/board-views/gantt";
@@ -11,16 +14,31 @@ import { IssueSpreadsheetView } from "@/components/issues/board-views/spreadshee
import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root";
import { IssuePeekOverview } from "@/components/issues/peek-overview";
// mobx store
import { useMobxStore, useUser } from "@/hooks/store";
import { RootStore } from "@/store/root.store";
import { useIssue, useUser, useProject, useIssueDetails } from "@/hooks/store";
// assets
import SomethingWentWrongImage from "public/something-went-wrong.svg";
export const ProjectDetailsView = observer(() => {
const router = useRouter();
const { workspace_slug, project_slug, states, labels, priorities, peekId } = router.query;
type ProjectDetailsViewProps = {
workspaceSlug: string;
projectId: string;
peekId: string;
};
const { issue: issueStore, project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props) => {
const { workspaceSlug, projectId, peekId } = props;
// router
const params = useParams();
// store hooks
const { fetchPublicIssues } = useIssue();
const { activeLayout } = useProject();
// fetching public issues
useSWR(
workspaceSlug && projectId ? "PROJECT_PUBLIC_ISSUES" : null,
workspaceSlug && projectId ? () => fetchPublicIssues(workspaceSlug, projectId, params) : null
);
// store hooks
const issueStore = useIssue();
const issueDetailStore = useIssueDetails();
const { data: currentUser, fetchCurrentUser } = useUser();
useEffect(() => {
@@ -30,25 +48,14 @@ export const ProjectDetailsView = observer(() => {
}, [currentUser, fetchCurrentUser]);
useEffect(() => {
if (workspace_slug && project_slug) {
const params = {
state: states || null,
labels: labels || null,
priority: priorities || null,
};
issueStore.fetchPublicIssues(workspace_slug?.toString(), project_slug.toString(), params);
}
}, [workspace_slug, project_slug, issueStore, states, labels, priorities]);
useEffect(() => {
if (peekId && workspace_slug && project_slug) {
if (peekId && workspaceSlug && projectId) {
issueDetailStore.setPeekId(peekId.toString());
}
}, [peekId, issueDetailStore, project_slug, workspace_slug]);
}, [peekId, issueDetailStore, projectId, workspaceSlug]);
return (
<div className="relative h-full w-full overflow-hidden">
{workspace_slug && <IssuePeekOverview />}
{workspaceSlug && <IssuePeekOverview />}
{issueStore?.loader && !issueStore.issues ? (
<div className="py-10 text-center text-sm text-custom-text-100">Loading...</div>
@@ -67,24 +74,24 @@ export const ProjectDetailsView = observer(() => {
</div>
</div>
) : (
projectStore?.activeBoard && (
activeLayout && (
<div className="relative flex h-full w-full flex-col overflow-hidden">
{/* applied filters */}
<IssueAppliedFilters />
{projectStore?.activeBoard === "list" && (
{activeLayout === "list" && (
<div className="relative h-full w-full overflow-y-auto">
<IssueListView />
<IssueListView workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
)}
{projectStore?.activeBoard === "kanban" && (
{activeLayout === "kanban" && (
<div className="relative mx-auto h-full w-full p-5">
<IssueKanbanView />
<IssueKanbanView workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
)}
{projectStore?.activeBoard === "calendar" && <IssueCalendarView />}
{projectStore?.activeBoard === "spreadsheet" && <IssueSpreadsheetView />}
{projectStore?.activeBoard === "gantt" && <IssueGanttView />}
{activeLayout === "calendar" && <IssueCalendarView />}
{activeLayout === "spreadsheet" && <IssueSpreadsheetView />}
{activeLayout === "gantt" && <IssueGanttView />}
</div>
)
)}
+20
View File
@@ -0,0 +1,20 @@
import { ILayoutDisplayFiltersOptions } from "@/types/issue-filters";
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions };
} = {
issues: {
list: {
filters: ["priority", "state", "labels"],
display_properties: null,
display_filters: null,
extra_options: null,
},
kanban: {
filters: ["priority", "state", "labels"],
display_properties: null,
display_filters: null,
extra_options: null,
},
},
};
+3
View File
@@ -3,6 +3,9 @@ import { twMerge } from "tailwind-merge";
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
+6 -3
View File
@@ -1,4 +1,7 @@
export * from "./user-mobx-provider";
export * from "./use-instance";
export * from "./user";
export * from "./use-project";
export * from "./use-issue";
export * from "./use-user";
export * from "./use-user-profile";
export * from "./use-issue-details";
export * from "./use-issue-filter";
+3 -2
View File
@@ -1,10 +1,11 @@
import { useContext } from "react";
// lib
import { StoreContext } from "@/lib/app-providers";
// store
import { StoreContext } from "@/lib/store-context";
import { IInstanceStore } from "@/store/instance.store";
export const useInstance = (): IInstanceStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useInstance must be used within StoreProvider");
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
return context.instance;
};
+11
View File
@@ -0,0 +1,11 @@
import { useContext } from "react";
// lib
import { StoreContext } from "@/lib/app-providers";
// store
import { IIssueDetailStore } from "@/store/issue-detail.store";
export const useIssueDetails = (): IIssueDetailStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
return context.issueDetail;
};
+11
View File
@@ -0,0 +1,11 @@
import { useContext } from "react";
// lib
import { StoreContext } from "@/lib/app-providers";
// store
import { IIssueFilterStore } from "@/store/issue-filters.store";
export const useIssueFilter = (): IIssueFilterStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
return context.issueFilter;
};
+11
View File
@@ -0,0 +1,11 @@
import { useContext } from "react";
// lib
import { StoreContext } from "@/lib/app-providers";
// store
import { IIssueStore } from "@/store/issue.store";
export const useIssue = (): IIssueStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
return context.issue;
};
+11
View File
@@ -0,0 +1,11 @@
import { useContext } from "react";
// lib
import { StoreContext } from "@/lib/app-providers";
// store
import { IProjectStore } from "@/store/project.store";
export const useProject = (): IProjectStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
return context.project;
};
@@ -1,10 +1,11 @@
import { useContext } from "react";
// lib
import { StoreContext } from "@/lib/app-providers";
// store
import { StoreContext } from "@/lib/store-context";
import { IProfileStore } from "@/store/user/profile.store";
import { IProfileStore } from "@/store/profile.store";
export const useUserProfile = (): IProfileStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
return context.user.userProfile;
return context.user.profile;
};
@@ -1,7 +1,8 @@
import { useContext } from "react";
// lib
import { StoreContext } from "@/lib/app-providers";
// store
import { StoreContext } from "@/lib/store-context";
import { IUserStore } from "@/store/user";
import { IUserStore } from "@/store/user.store";
export const useUser = (): IUserStore => {
const context = useContext(StoreContext);
-10
View File
@@ -1,10 +0,0 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/lib/store-context";
import { RootStore } from "@/store/root.store";
export const useMobxStore = (): RootStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useMobxStore must be used within StoreProvider");
return context;
};
-2
View File
@@ -1,2 +0,0 @@
export * from "./use-user";
export * from "./use-user-profile";
-13
View File
@@ -1,13 +0,0 @@
import { useMobxStore } from "@/hooks/store";
import { RootStore } from "@/store/root.store";
const useEditorSuggestions = () => {
const { mentionsStore }: RootStore = useMobxStore();
return {
// mentionSuggestions: mentionsStore.mentionSuggestions,
mentionHighlights: mentionsStore.mentionHighlights,
};
};
export default useEditorSuggestions;
-32
View File
@@ -1,32 +0,0 @@
import Image from "next/image";
// mobx
import { observer } from "mobx-react-lite";
import planeLogo from "public/plane-logo.svg";
// components
import IssueNavbar from "@/components/issues/navbar";
const ProjectLayout = ({ children }: { children: React.ReactNode }) => (
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
<IssueNavbar />
</div>
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
<a
href="https://plane.so"
className="fixed bottom-2.5 right-5 !z-[999999] flex items-center gap-1 rounded border border-custom-border-200 bg-custom-background-100 px-2 py-1 shadow-custom-shadow-2xs"
target="_blank"
rel="noreferrer noopener"
>
<div className="relative grid h-6 w-6 place-items-center">
<Image src={planeLogo} alt="Plane logo" className="h-6 w-6" height="24" width="24" />
</div>
<div className="text-xs">
Powered by <span className="font-semibold">Plane Deploy</span>
</div>
</a>
</div>
);
export default observer(ProjectLayout);
+38
View File
@@ -0,0 +1,38 @@
"use client";
import { ReactNode, createContext } from "react";
import { ThemeProvider } from "next-themes";
// store
import { RootStore } from "@/store/root.store";
let rootStore = new RootStore();
export const StoreContext = createContext(rootStore);
function initializeStore(initialData = {}) {
const singletonRootStore = rootStore ?? new RootStore();
// If your page has Next.js data fetching methods that use a Mobx store, it will
// get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details
if (initialData) {
singletonRootStore.hydrate(initialData);
}
// For SSG and SSR always create a new store
if (typeof window === "undefined") return singletonRootStore;
// Create the store once in the client
if (!rootStore) rootStore = singletonRootStore;
return singletonRootStore;
}
export type AppProviderProps = {
children: ReactNode;
initialState: any;
};
export const AppProvider = ({ children, initialState = {} }: AppProviderProps) => {
const store = initializeStore(initialState);
return (
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
</ThemeProvider>
);
};
-1
View File
@@ -1 +0,0 @@
export const init = {};
-19
View File
@@ -1,19 +0,0 @@
import { ReactElement, createContext } from "react";
// mobx store
import { RootStore } from "@/store/root.store";
export let rootStore = new RootStore();
export const StoreContext = createContext<RootStore>(rootStore);
const initializeStore = () => {
const singletonRootStore = rootStore ?? new RootStore();
if (typeof window === "undefined") return singletonRootStore;
if (!rootStore) rootStore = singletonRootStore;
return singletonRootStore;
};
export const StoreProvider = ({ children }: { children: ReactElement }) => {
const store = initializeStore();
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
};
+2 -1
View File
@@ -1,6 +1,6 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import useSWR from "swr";
import { Spinner } from "@plane/ui";
// helpers
@@ -32,6 +32,7 @@ export const AuthWrapper: FC<TAuthWrapper> = observer((props) => {
<Spinner />
</div>
);
if (pageType === EPageTypes.PUBLIC) return <>{children}</>;
if (pageType === EPageTypes.INIT) {
-1
View File
@@ -1,2 +1 @@
export * from "./instance-wrapper";
export * from "./auth-wrapper";
-34
View File
@@ -1,34 +0,0 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// ui
import { Spinner } from "@plane/ui";
// components
import { InstanceNotReady } from "@/components/instance";
// hooks
import { useInstance } from "@/hooks/store";
type TInstanceWrapper = {
children: ReactNode;
};
export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
const { children } = props;
// hooks
const { isLoading, instance, fetchInstanceInfo } = useInstance();
const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
revalidateOnFocus: false,
});
if (isSWRLoading || isLoading)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<Spinner />
</div>
);
if (instance?.instance?.is_setup_done === false) return <InstanceNotReady />;
return <>{children}</>;
});
-42
View File
@@ -1,42 +0,0 @@
// next imports
import { observer } from "mobx-react-lite";
import Image from "next/image";
// hooks
import { useInstance } from "@/hooks/store";
// images
import notFoundImage from "public/404.svg";
const Custom404Error = observer(() => {
// hooks
const { instance } = useInstance();
const redirectionUrl = instance?.config?.app_base_url || "/";
return (
<div className="relative flex h-full min-h-screen w-screen items-center justify-center py-5">
<div className="max-w-[700px] space-y-5">
<div className="flex flex-col items-center gap-3 text-center">
<div className="relative h-[240px] w-[240px]">
<Image src={notFoundImage} layout="fill" alt="404- Page not found" />
</div>
<div className="text-xl font-medium">Oops! Something went wrong.</div>
<div className="text-sm text-custom-text-200">
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
temporarily unavailable.
</div>
</div>
<div className="flex items-center justify-center text-center">
<a
href={redirectionUrl}
className="cursor-pointer select-none rounded-sm border border-gray-200 bg-gray-50 p-1.5 px-2.5 text-sm font-medium text-gray-700 transition-all hover:scale-105 hover:bg-gray-100 hover:text-gray-800"
>
Go to your Workspace
</a>
</div>
</div>
</div>
);
});
export default Custom404Error;
@@ -1,47 +0,0 @@
import Head from "next/head";
import { useRouter } from "next/router";
import useSWR from "swr";
// components
import { ProjectDetailsView } from "@/components/views";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useMobxStore } from "@/hooks/store";
// layouts
import ProjectLayout from "@/layouts/project-layout";
// wrappers
import { AuthWrapper } from "@/lib/wrappers";
const WorkspaceProjectPage = (props: any) => {
const SITE_TITLE = props?.project_settings?.project_details?.name || "Plane | Deploy";
const router = useRouter();
const { workspace_slug, project_slug, states, labels, priorities } = router.query;
const { project: projectStore, issue: issueStore } = useMobxStore();
useSWR("REVALIDATE_ALL", () => {
if (workspace_slug && project_slug) {
projectStore.fetchProjectSettings(workspace_slug.toString(), project_slug.toString());
const params = {
state: states || null,
labels: labels || null,
priority: priorities || null,
};
issueStore.fetchPublicIssues(workspace_slug.toString(), project_slug.toString(), params);
}
});
return (
<AuthWrapper pageType={EPageTypes.AUTHENTICATED}>
<ProjectLayout>
<Head>
<title>{SITE_TITLE}</title>
</Head>
<ProjectDetailsView />
</ProjectLayout>
</AuthWrapper>
);
};
export default WorkspaceProjectPage;
-5
View File
@@ -1,5 +0,0 @@
const WorkspaceProjectPage = () => (
<div className="relative flex h-screen w-screen items-center justify-center text-5xl">Plane Workspace Space</div>
);
export default WorkspaceProjectPage;

Some files were not shown because too many files have changed in this diff Show More