Merge pull request #35 from makeplane/develop
promote: develop to preview
This commit is contained in:
+4
-25
@@ -33,8 +33,8 @@ The backend is a django project which is kept inside apiserver
|
||||
1. Clone the repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/makeplane/plane
|
||||
cd plane
|
||||
git clone https://github.com/makeplane/plane.git [folder-name]
|
||||
cd [folder-name]
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
@@ -44,33 +44,12 @@ chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||
3. Start the containers
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||
docker compose -f docker-compose-local.yml up
|
||||
```
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
|
||||
```
|
||||
|
||||
4. Run Docker compose up
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. Install dependencies
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
6. Run the web app in development mode
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Missing a Feature?
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
|
||||
|
||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting/docker-compose).
|
||||
|
||||
## ⚡️ Contributors Quick Start
|
||||
|
||||
@@ -63,7 +63,7 @@ Thats it!
|
||||
|
||||
## 🍙 Self Hosting
|
||||
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
|
||||
@@ -49,5 +49,5 @@ USER captain
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
# CMD [ "./bin/takeoff" ]
|
||||
CMD [ "./bin/takeoff.local" ]
|
||||
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
python manage.py wait_for_db
|
||||
python manage.py migrate
|
||||
|
||||
# Create the default bucket
|
||||
#!/bin/bash
|
||||
|
||||
# Collect system information
|
||||
HOSTNAME=$(hostname)
|
||||
MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
|
||||
CPU_INFO=$(cat /proc/cpuinfo)
|
||||
MEMORY_INFO=$(free -h)
|
||||
DISK_INFO=$(df -h)
|
||||
|
||||
# Concatenate information and compute SHA-256 hash
|
||||
SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
|
||||
|
||||
# Export the variables
|
||||
export MACHINE_SIGNATURE=$SIGNATURE
|
||||
|
||||
# Register instance
|
||||
python manage.py register_instance $MACHINE_SIGNATURE
|
||||
# Load the configuration variable
|
||||
python manage.py configure_instance
|
||||
|
||||
# Create the default bucket
|
||||
python manage.py create_bucket
|
||||
|
||||
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Python imports
|
||||
import json
|
||||
import secrets
|
||||
|
||||
# Django imports
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import Instance, InstanceAdmin
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Check if instance in registered else register"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Positional argument
|
||||
parser.add_argument("admin_email", type=str, help="Admin Email")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
with open("package.json", "r") as file:
|
||||
# Load JSON content from the file
|
||||
data = json.load(file)
|
||||
|
||||
admin_email = options.get("admin_email", False)
|
||||
|
||||
if not admin_email:
|
||||
raise CommandError("admin email is required")
|
||||
|
||||
user_count = User.objects.filter(is_bot=False).count()
|
||||
|
||||
user = User.objects.filter(email=admin_email).first()
|
||||
if user is None:
|
||||
raise CommandError("User not found")
|
||||
|
||||
try:
|
||||
# Check if the instance is registered
|
||||
instance = Instance.objects.first()
|
||||
|
||||
if instance is None:
|
||||
instance = Instance.objects.create(
|
||||
instance_name="Plane Cloud US",
|
||||
instance_id=secrets.token_hex(12),
|
||||
license_key=None,
|
||||
api_key=secrets.token_hex(8),
|
||||
version=data.get("version"),
|
||||
last_checked_at=timezone.now(),
|
||||
user_count=user_count,
|
||||
is_verified=True,
|
||||
is_setup_done=True,
|
||||
is_signup_screen_visited=True,
|
||||
)
|
||||
|
||||
# Get or create an instance admin
|
||||
_ , created = InstanceAdmin.objects.get_or_create(
|
||||
user=user, instance=instance, role=20, is_verified=True
|
||||
)
|
||||
|
||||
if not created:
|
||||
raise CommandError("given email is already an instance admin")
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Successful"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise CommandError(
|
||||
"Failure"
|
||||
)
|
||||
@@ -343,13 +343,6 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
|
||||
if str(token) == str(user_token):
|
||||
user = User.objects.get(email=email)
|
||||
if not user.is_active:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# Send event
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
|
||||
@@ -42,6 +42,7 @@ from plane.app.permissions import (
|
||||
WorkspaceUserPermission,
|
||||
ProjectBasePermission,
|
||||
ProjectMemberPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
|
||||
from plane.db.models import (
|
||||
@@ -144,6 +145,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"project_projectmember",
|
||||
queryset=ProjectMember.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
is_active=True,
|
||||
).select_related("member"),
|
||||
to_attr="members_list",
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@@ -159,16 +170,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
projects = (
|
||||
self.get_queryset()
|
||||
.annotate(sort_order=Subquery(sort_order_query))
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"project_projectmember",
|
||||
queryset=ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
).select_related("member"),
|
||||
to_attr="members_list",
|
||||
)
|
||||
)
|
||||
.order_by("sort_order", "name")
|
||||
)
|
||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
||||
@@ -602,6 +603,18 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "leave":
|
||||
self.permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
else:
|
||||
self.permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
|
||||
return super(ProjectMemberViewSet, self).get_permissions()
|
||||
|
||||
search_fields = [
|
||||
"member__display_name",
|
||||
"member__first_name",
|
||||
@@ -666,6 +679,25 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
|
||||
# Check if the user is already a member of the project and is inactive
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member_id=member.get("member_id"),
|
||||
is_active=False,
|
||||
).exists():
|
||||
member_detail = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member_id=member.get("member_id"),
|
||||
is_active=False,
|
||||
)
|
||||
# Check if the user has not deactivated the account
|
||||
user = User.objects.filter(pk=member.get("member_id")).first()
|
||||
if user.is_active:
|
||||
member_detail.is_active = True
|
||||
member_detail.save(update_fields=["is_active"])
|
||||
|
||||
project_members = ProjectMember.objects.bulk_create(
|
||||
bulk_project_members,
|
||||
batch_size=10,
|
||||
|
||||
@@ -70,6 +70,7 @@ from plane.app.permissions import (
|
||||
WorkSpaceAdminPermission,
|
||||
WorkspaceEntityPermission,
|
||||
WorkspaceViewerPermission,
|
||||
WorkspaceUserPermission,
|
||||
)
|
||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
@@ -495,6 +496,18 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "leave":
|
||||
self.permission_classes = [
|
||||
WorkspaceUserPermission,
|
||||
]
|
||||
else:
|
||||
self.permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
return super(WorkSpaceMemberViewSet, self).get_permissions()
|
||||
|
||||
search_fields = [
|
||||
"member__display_name",
|
||||
"member__first_name",
|
||||
|
||||
@@ -65,7 +65,7 @@ def send_export_email(email, slug, csv_buffer, rows):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -51,7 +51,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -41,7 +41,7 @@ def magic_link(email, key, token, current_site):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -60,7 +60,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -70,7 +70,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -19,8 +19,11 @@ urlpatterns = [
|
||||
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
try:
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
urlpatterns = [
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -12,7 +12,6 @@ volumes:
|
||||
|
||||
services:
|
||||
plane-redis:
|
||||
container_name: plane-redis
|
||||
image: redis:6.2.7-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -21,7 +20,6 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
plane-minio:
|
||||
container_name: plane-minio
|
||||
image: minio/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -36,7 +34,6 @@ services:
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
|
||||
plane-db:
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -53,7 +50,6 @@ services:
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
|
||||
web:
|
||||
container_name: web
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./web/Dockerfile.dev
|
||||
@@ -61,8 +57,7 @@ services:
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- .:/app
|
||||
command: yarn dev --filter=web
|
||||
- ./web:/app/web
|
||||
env_file:
|
||||
- ./web/.env
|
||||
depends_on:
|
||||
@@ -73,22 +68,17 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./space/Dockerfile.dev
|
||||
container_name: space
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- .:/app
|
||||
command: yarn dev --filter=space
|
||||
env_file:
|
||||
- ./space/.env
|
||||
- ./space:/app/space
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
api:
|
||||
container_name: api
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -99,7 +89,7 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
|
||||
# command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@@ -107,7 +97,6 @@ services:
|
||||
- plane-redis
|
||||
|
||||
worker:
|
||||
container_name: bgworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -127,7 +116,6 @@ services:
|
||||
- plane-redis
|
||||
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -147,10 +135,9 @@ services:
|
||||
- plane-redis
|
||||
|
||||
proxy:
|
||||
container_name: proxy
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile.dev
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM nginx:1.25.0-alpine
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf.dev /etc/nginx/nginx.conf.template
|
||||
|
||||
COPY ./env.sh /docker-entrypoint.sh
|
||||
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
# Update all environment variables
|
||||
CMD ["/docker-entrypoint.sh"]
|
||||
@@ -18,7 +18,7 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location /space/ {
|
||||
location /spaces/ {
|
||||
proxy_pass http://localhost:4000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
events {
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /www/data/;
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
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;
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:3000/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://api:8000/api/;
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
rewrite ^/spaces/?$ /spaces/login break;
|
||||
proxy_pass http://space:4000/spaces/;
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
proxy_pass http://plane-minio:9000/uploads/;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ export LC_ALL=C
|
||||
export LC_CTYPE=C
|
||||
|
||||
cp ./web/.env.example ./web/.env
|
||||
cp ./space/.env.example ./space/.env
|
||||
cp ./apiserver/.env.example ./apiserver/.env
|
||||
|
||||
# Generate the SECRET_KEY that will be used by django
|
||||
|
||||
@@ -7,5 +7,8 @@ WORKDIR /app
|
||||
COPY . .
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
EXPOSE 3000
|
||||
EXPOSE 4000
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/space/node_modules"]
|
||||
CMD ["yarn","dev", "--filter=space"]
|
||||
|
||||
@@ -130,7 +130,7 @@ export const SelfHostedSignInForm: React.FC<Props> = (props) => {
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" variant="primary" className="w-full" size="xl" loading={isSubmitting}>
|
||||
Go to board
|
||||
Continue
|
||||
</Button>
|
||||
<p className="text-xs text-onboarding-text-200">
|
||||
When you click the button above, you agree with our{" "}
|
||||
|
||||
@@ -8,4 +8,5 @@ COPY . .
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
EXPOSE 3000
|
||||
VOLUME [ "/app/node_modules", "/app/web/node_modules" ]
|
||||
CMD ["yarn", "dev", "--filter=web"]
|
||||
|
||||
@@ -130,7 +130,7 @@ export const SelfHostedSignInForm: React.FC<Props> = (props) => {
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" variant="primary" className="w-full" size="xl" loading={isSubmitting}>
|
||||
Go to workspace
|
||||
Continue
|
||||
</Button>
|
||||
<p className="text-xs text-onboarding-text-200">
|
||||
When you click the button above, you agree with our{" "}
|
||||
|
||||
@@ -27,18 +27,17 @@ export const ProjectAnalyticsModal: React.FC<Props> = observer((props) => {
|
||||
return (
|
||||
<Transition.Root appear show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="transition-transform duration-300"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
{/* TODO: fix full screen mode */}
|
||||
<Dialog.Panel
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="transition-transform duration-300"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<div
|
||||
className={`fixed right-0 top-0 z-20 h-full bg-custom-background-100 shadow-custom-shadow-md ${
|
||||
fullScreen ? "w-full p-2" : "w-1/2"
|
||||
}`}
|
||||
@@ -61,9 +60,9 @@ export const ProjectAnalyticsModal: React.FC<Props> = observer((props) => {
|
||||
projectDetails={projectDetails}
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// components
|
||||
import { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
|
||||
import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
|
||||
// types
|
||||
import { IIssue, IPageBlock } from "types";
|
||||
|
||||
@@ -42,6 +42,7 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
const responseRef = useRef<any>(null);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -115,6 +116,10 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
|
||||
editorRef.current?.setEditorValue(htmlContent ?? `<p>${content}</p>`);
|
||||
}, [htmlContent, editorRef, content]);
|
||||
|
||||
useEffect(() => {
|
||||
responseRef.current?.setEditorValue(`<p>${response}</p>`);
|
||||
}, [response, responseRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute ${inset} z-20 flex w-full flex-col space-y-4 overflow-hidden rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
|
||||
@@ -137,11 +142,12 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
|
||||
{response !== "" && (
|
||||
<div className="page-block-section text-sm">
|
||||
Response:
|
||||
<RichReadOnlyEditor
|
||||
<RichReadOnlyEditorWithRef
|
||||
value={`<p>${response}</p>`}
|
||||
customClassName="-mx-3 -my-3"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
ref={responseRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,8 +14,8 @@ type Props = {
|
||||
handleClose: () => void;
|
||||
data?: ILinkDetails | null;
|
||||
status: boolean;
|
||||
createIssueLink: (formData: IIssueLink | ModuleLink) => Promise<void>;
|
||||
updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise<void>;
|
||||
createIssueLink: (formData: IIssueLink | ModuleLink) => Promise<ILinkDetails> | Promise<void> | void;
|
||||
updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise<ILinkDetails> | Promise<void> | void;
|
||||
};
|
||||
|
||||
const defaultValues: IIssueLink | ModuleLink = {
|
||||
@@ -31,7 +31,7 @@ export const LinkModal: FC<Props> = (props) => {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
} = useForm<ModuleLink>({
|
||||
} = useForm<IIssueLink | ModuleLink>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
@@ -158,8 +158,8 @@ export const LinkModal: FC<Props> = (props) => {
|
||||
? "Updating Link..."
|
||||
: "Update Link"
|
||||
: isSubmitting
|
||||
? "Adding Link..."
|
||||
: "Add Link"}
|
||||
? "Adding Link..."
|
||||
: "Add Link"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -75,7 +75,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useSWR(
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null,
|
||||
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null
|
||||
);
|
||||
@@ -94,7 +94,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
// : null
|
||||
// ) as { data: IIssue[] | undefined };
|
||||
|
||||
if (!cycle)
|
||||
if (!cycle && isLoading)
|
||||
return (
|
||||
<Loader>
|
||||
<Loader.Item height="250px" />
|
||||
@@ -187,12 +187,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "rgb(var(--color-text-200))"
|
||||
: ""
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "rgb(var(--color-text-200))"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
@@ -207,12 +207,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
cycleStatus === "current"
|
||||
? "bg-green-600/5 text-green-600"
|
||||
: cycleStatus === "upcoming"
|
||||
? "bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
? "bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
|
||||
@@ -23,6 +23,7 @@ import { ICycle } from "types";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// constants
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export interface ICyclesBoardCard {
|
||||
workspaceSlug: string;
|
||||
@@ -36,6 +37,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
const {
|
||||
cycle: cycleStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: userStore,
|
||||
} = useMobxStore();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -49,6 +51,9 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
const isDateValid = cycle.start_date || cycle.end_date;
|
||||
|
||||
const { currentProjectRole } = userStore;
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||
@@ -68,8 +73,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
? cycleTotalIssues === 0
|
||||
? "0 Issue"
|
||||
: cycleTotalIssues === cycle.completed_issues
|
||||
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
|
||||
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
|
||||
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
|
||||
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
|
||||
: "0 Issue";
|
||||
|
||||
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -235,17 +240,18 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
<span className="text-xs text-custom-text-400">No due date</span>
|
||||
)}
|
||||
<div className="z-10 flex items-center gap-1.5">
|
||||
{cycle.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
{isEditingAllowed &&
|
||||
(cycle.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
))}
|
||||
<CustomMenu width="auto" ellipsis className="z-10">
|
||||
{!isCompleted && (
|
||||
{!isCompleted && isEditingAllowed && (
|
||||
<>
|
||||
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
|
||||
@@ -24,6 +24,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { ICycle } from "types";
|
||||
// constants
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type TCyclesListItem = {
|
||||
cycle: ICycle;
|
||||
@@ -41,6 +42,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
const {
|
||||
cycle: cycleStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: userStore,
|
||||
} = useMobxStore();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -53,6 +55,9 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
|
||||
const { currentProjectRole } = userStore;
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const cycleTotalIssues =
|
||||
@@ -226,19 +231,19 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{cycle.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
{isEditingAllowed &&
|
||||
(cycle.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
))}
|
||||
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
{!isCompleted && (
|
||||
{!isCompleted && isEditingAllowed && (
|
||||
<>
|
||||
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
|
||||
@@ -97,7 +97,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
id="cycle_description"
|
||||
name="description"
|
||||
placeholder="Description..."
|
||||
className="h-24 w-full resize-none text-sm"
|
||||
className="!h-24 w-full resize-none text-sm"
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
@@ -135,18 +135,12 @@ export const CycleForm: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 ">
|
||||
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 ">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{data
|
||||
? isSubmitting
|
||||
? "Updating Cycle..."
|
||||
: "Update Cycle"
|
||||
: isSubmitting
|
||||
? "Creating Cycle..."
|
||||
: "Create Cycle"}
|
||||
{data ? (isSubmitting ? "Updating" : "Update cycle") : isSubmitting ? "Creating" : "Create cycle"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -49,11 +49,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
state: "SUCCESS",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Error in creating cycle. Please try again.",
|
||||
message: err.detail ?? "Error in creating cycle. Please try again.",
|
||||
});
|
||||
postHogEventTracker("CYCLE_CREATE", {
|
||||
state: "FAILED",
|
||||
@@ -73,11 +73,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
message: "Cycle updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Error in updating cycle. Please try again.",
|
||||
message: err.detail ?? "Error in updating cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
} from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
// fetch-keys
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
|
||||
@@ -53,6 +55,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
cycle: cycleDetailsStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
|
||||
@@ -270,10 +273,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</Loader>
|
||||
);
|
||||
|
||||
const endDate = new Date(cycleDetails.end_date ?? "");
|
||||
const startDate = new Date(cycleDetails.start_date ?? "");
|
||||
const endDate = new Date(watch("end_date") ?? cycleDetails.end_date ?? "");
|
||||
const startDate = new Date(watch("start_date") ?? cycleDetails.start_date ?? "");
|
||||
|
||||
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
|
||||
const areYearsEqual =
|
||||
startDate.getFullYear() === endDate.getFullYear() || isNaN(startDate.getFullYear()) || isNaN(endDate.getFullYear());
|
||||
|
||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||
|
||||
@@ -281,10 +285,12 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
cycleDetails.total_issues === 0
|
||||
? "0 Issue"
|
||||
: cycleDetails.total_issues === cycleDetails.completed_issues
|
||||
? cycleDetails.total_issues > 1
|
||||
? `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||
? cycleDetails.total_issues > 1
|
||||
? `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -312,7 +318,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<button onClick={handleCopyText}>
|
||||
<LinkIcon className="h-3 w-3 text-custom-text-300" />
|
||||
</button>
|
||||
{!isCompleted && (
|
||||
{!isCompleted && isEditingAllowed && (
|
||||
<CustomMenu width="lg" placement="bottom-end" ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
@@ -349,8 +355,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<div className="relative flex h-full w-52 items-center gap-2.5">
|
||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||
<Popover.Button
|
||||
disabled={isCompleted ?? false}
|
||||
className="cursor-default text-sm font-medium text-custom-text-300"
|
||||
className={`text-sm font-medium text-custom-text-300 ${
|
||||
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
||||
}`}
|
||||
disabled={isCompleted || !isEditingAllowed}
|
||||
>
|
||||
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
|
||||
</Popover.Button>
|
||||
@@ -373,10 +381,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
handleStartDateChange(val);
|
||||
}
|
||||
}}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
startDate={watch("start_date") ?? watch("end_date") ?? null}
|
||||
endDate={watch("end_date") ?? watch("start_date") ?? null}
|
||||
maxDate={new Date(`${watch("end_date")}`)}
|
||||
selectsStart
|
||||
selectsStart={watch("end_date") ? true : false}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
@@ -385,8 +393,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||
<>
|
||||
<Popover.Button
|
||||
disabled={isCompleted ?? false}
|
||||
className="cursor-default text-sm font-medium text-custom-text-300"
|
||||
className={`text-sm font-medium text-custom-text-300 ${
|
||||
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
||||
}`}
|
||||
disabled={isCompleted || !isEditingAllowed}
|
||||
>
|
||||
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
|
||||
</Popover.Button>
|
||||
@@ -409,10 +419,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
handleEndDateChange(val);
|
||||
}
|
||||
}}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
startDate={watch("start_date") ?? watch("end_date") ?? null}
|
||||
endDate={watch("end_date") ?? watch("start_date") ?? null}
|
||||
minDate={new Date(`${watch("start_date")}`)}
|
||||
selectsEnd
|
||||
selectsEnd={watch("start_date") ? true : false}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
@@ -468,7 +478,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
{progressPercentage ? (
|
||||
<span className="flex h-5 w-9 items-center justify-center rounded bg-amber-50 text-xs font-medium text-amber-500">
|
||||
<span className="flex h-5 w-9 items-center justify-center rounded bg-amber-500/20 text-xs font-medium text-amber-500">
|
||||
{progressPercentage ? `${progressPercentage}%` : ""}
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -15,7 +15,6 @@ import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blocks: IGanttBlock[] | null;
|
||||
enableReorder: boolean;
|
||||
@@ -27,11 +26,20 @@ type Props = {
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
disableIssueCreation?: boolean;
|
||||
};
|
||||
|
||||
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { title, blockUpdateHandler, blocks, enableReorder, enableQuickIssueCreate, quickAddCallback, viewId } = props;
|
||||
const {
|
||||
blockUpdateHandler,
|
||||
blocks,
|
||||
enableReorder,
|
||||
enableQuickIssueCreate,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
disableIssueCreation,
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query;
|
||||
@@ -160,7 +168,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</>
|
||||
{enableQuickIssueCreate && (
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<GanttInlineCreateIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -42,6 +43,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
cycleIssuesFilter: { issueFilters, updateFilters },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout;
|
||||
@@ -99,6 +101,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const cyclesList = cycleStore.projectCycles;
|
||||
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -150,7 +155,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
key={cycle.id}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)}
|
||||
>
|
||||
{truncateText(cycle.name, 40)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ContrastIcon className="h-3 w-3" />
|
||||
{truncateText(cycle.name, 40)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
@@ -187,19 +195,24 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
|
||||
{canUserCreateIssue && (
|
||||
<>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const CyclesHeader: FC = observer(() => {
|
||||
// router
|
||||
@@ -16,11 +17,15 @@ export const CyclesHeader: FC = observer(() => {
|
||||
// store
|
||||
const {
|
||||
project: projectStore,
|
||||
user: { currentProjectRole },
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
const canUserCreateCycle =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@@ -50,19 +55,21 @@ export const CyclesHeader: FC = observer(() => {
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLES_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateCycleModal(true);
|
||||
}}
|
||||
>
|
||||
Add Cycle
|
||||
</Button>
|
||||
</div>
|
||||
{canUserCreateCycle && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLES_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateCycleModal(true);
|
||||
}}
|
||||
>
|
||||
Add Cycle
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
const GLOBAL_VIEW_LAYOUTS = [
|
||||
{ key: "list", title: "List", link: "/workspace-views", icon: List },
|
||||
@@ -38,7 +39,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
||||
workspace: { workspaceLabels },
|
||||
workspaceMember: { workspaceMembers },
|
||||
project: { workspaceProjects },
|
||||
|
||||
user: { currentWorkspaceRole },
|
||||
workspaceGlobalIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
@@ -77,6 +78,8 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
||||
[workspaceSlug, updateFilters]
|
||||
);
|
||||
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
||||
@@ -142,10 +145,11 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
||||
</FiltersDropdown>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}>
|
||||
New View
|
||||
</Button>
|
||||
{isAuthorizedUser && (
|
||||
<Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}>
|
||||
New View
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ProjectAnalyticsModal } from "components/analytics";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui";
|
||||
// icons
|
||||
import { ArrowRight, ContrastIcon, Plus } from "lucide-react";
|
||||
import { ArrowRight, Plus } from "lucide-react";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
@@ -21,6 +21,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -41,6 +42,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
trackEvent: { setTrackElement },
|
||||
projectLabel: { projectLabels },
|
||||
moduleIssuesFilter: { issueFilters, updateFilters },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const { currentProjectDetails } = projectStore;
|
||||
@@ -100,6 +102,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined;
|
||||
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -138,7 +143,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<ContrastIcon className="h-3 w-3" />
|
||||
<DiceIcon className="h-3 w-3" />
|
||||
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
|
||||
</>
|
||||
}
|
||||
@@ -151,7 +156,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
key={module.id}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)}
|
||||
>
|
||||
{truncateText(module.name, 40)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<DiceIcon className="h-3 w-3" />
|
||||
{truncateText(module.name, 40)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
@@ -188,19 +196,24 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("MODULE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
|
||||
{canUserCreateIssue && (
|
||||
<>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("MODULE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||
|
||||
@@ -11,17 +11,25 @@ import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// constants
|
||||
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ModulesListHeader: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store
|
||||
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const {
|
||||
project: projectStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
const { storedValue: modulesView, setValue: setModulesView } = useLocalStorage("modules_view", "grid");
|
||||
|
||||
const canUserCreateModule =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@@ -72,14 +80,16 @@ export const ModulesListHeader: React.FC = observer(() => {
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => commandPaletteStore.toggleCreateModuleModal(true)}
|
||||
>
|
||||
Add Module
|
||||
</Button>
|
||||
{canUserCreateModule && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
onClick={() => commandPaletteStore.toggleCreateModuleModal(true)}
|
||||
>
|
||||
Add Module
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -36,6 +37,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
// issue filters
|
||||
projectIssuesFilter: { issueFilters, updateFilters },
|
||||
projectIssues: {},
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
@@ -87,6 +89,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
|
||||
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
@@ -197,19 +202,24 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
|
||||
{canUserCreateIssue && (
|
||||
<>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Plus } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
@@ -16,7 +17,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
import { Plus } from "lucide-react";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
@@ -35,6 +36,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
viewIssuesFilter: { issueFilters, updateFilters },
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
@@ -85,6 +87,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
|
||||
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -133,7 +138,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
key={view.id}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/views/${view.id}`)}
|
||||
>
|
||||
{truncateText(view.name, 40)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<PhotoFilterIcon height={12} width={12} />
|
||||
{truncateText(view.name, 40)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
@@ -147,7 +155,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" disabled={!canUserCreateIssue}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
@@ -170,16 +179,18 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,15 +7,24 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectViewsHeader: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { project: projectStore, commandPalette } = useMobxStore();
|
||||
const {
|
||||
project: projectStore,
|
||||
commandPalette,
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
@@ -50,18 +59,20 @@ export const ProjectViewsHeader: React.FC = observer(() => {
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus className="h-3.5 w-3.5 stroke-2" />}
|
||||
onClick={() => commandPalette.toggleCreateViewModal(true)}
|
||||
>
|
||||
Create View
|
||||
</Button>
|
||||
{canUserCreateIssue && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus className="h-3.5 w-3.5 stroke-2" />}
|
||||
onClick={() => commandPalette.toggleCreateViewModal(true)}
|
||||
>
|
||||
Create View
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Breadcrumbs, Button } from "@plane/ui";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectsHeader = observer(() => {
|
||||
const router = useRouter();
|
||||
@@ -15,10 +17,13 @@ export const ProjectsHeader = observer(() => {
|
||||
project: projectStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentWorkspaceRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : [];
|
||||
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@@ -44,17 +49,18 @@ export const ProjectsHeader = observer(() => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
prependIcon={<Plus />}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECTS_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
}}
|
||||
>
|
||||
Add Project
|
||||
</Button>
|
||||
{isAuthorizedUser && (
|
||||
<Button
|
||||
prependIcon={<Plus />}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECTS_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
}}
|
||||
>
|
||||
Add Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -165,16 +165,16 @@ export const InboxMainContent: React.FC = observer(() => {
|
||||
issueStatus === -2
|
||||
? "border-yellow-500 bg-yellow-500/10 text-yellow-500"
|
||||
: issueStatus === -1
|
||||
? "border-red-500 bg-red-500/10 text-red-500"
|
||||
: issueStatus === 0
|
||||
? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "border-red-500 bg-red-500/10 text-red-500"
|
||||
: issueStatus === 0
|
||||
? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "border-red-500 bg-red-500/10 text-red-500"
|
||||
: "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: issueStatus === 1
|
||||
? "border-green-500 bg-green-500/10 text-green-500"
|
||||
: issueStatus === 2
|
||||
? "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: ""
|
||||
: "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: issueStatus === 1
|
||||
? "border-green-500 bg-green-500/10 text-green-500"
|
||||
: issueStatus === 2
|
||||
? "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{issueStatus === -2 ? (
|
||||
@@ -225,7 +225,7 @@ export const InboxMainContent: React.FC = observer(() => {
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-5 flex items-center">
|
||||
<div className="mb-2.5 flex items-center">
|
||||
{currentIssueState && (
|
||||
<StateGroupIcon
|
||||
className="mr-3 h-4 w-4"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTheme } from "next-themes";
|
||||
import { Button } from "@plane/ui";
|
||||
import { UserCog2 } from "lucide-react";
|
||||
// images
|
||||
import instanceSetupDone from "public/instance-setup-done.svg";
|
||||
import instanceSetupDone from "public/instance-setup-done.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 { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
@@ -84,10 +84,10 @@ export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
|
||||
disabled: isLoading || disabled,
|
||||
});
|
||||
|
||||
const maxFileSize = envConfig?.file_size_limit ?? MAX_FILE_SIZE;
|
||||
|
||||
const fileError =
|
||||
fileRejections.length > 0
|
||||
? `Invalid file type or size (max ${envConfig?.file_size_limit ?? MAX_FILE_SIZE / 1024 / 1024} MB)`
|
||||
: null;
|
||||
fileRejections.length > 0 ? `Invalid file type or size (max ${maxFileSize / 1024 / 1024} MB)` : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Dialog, Transition } from "@headlessui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
|
||||
@@ -18,6 +20,8 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteLoading(false);
|
||||
}, [isOpen]);
|
||||
@@ -31,7 +35,21 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
setIsDeleteLoading(true);
|
||||
if (onSubmit)
|
||||
await onSubmit()
|
||||
.then(() => onClose())
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
onClose();
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: "Failed to delete issue",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsDeleteLoading(false));
|
||||
};
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||
debouncedFormSave();
|
||||
}}
|
||||
required
|
||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
||||
className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
||||
hasError={Boolean(errors?.description)}
|
||||
role="textbox"
|
||||
disabled={!isAllowed}
|
||||
|
||||
@@ -226,10 +226,9 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
|
||||
reset({
|
||||
...defaultValues,
|
||||
project: projectId,
|
||||
...initialData,
|
||||
});
|
||||
}, [setFocus, initialData, projectId, reset]);
|
||||
}, [setFocus, initialData, reset]);
|
||||
|
||||
// update projectId in form when projectId changes
|
||||
useEffect(() => {
|
||||
@@ -629,8 +628,8 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
? "Updating Issue..."
|
||||
: "Update Issue"
|
||||
: isSubmitting
|
||||
? "Adding Issue..."
|
||||
: "Add Issue"}
|
||||
? "Adding Issue..."
|
||||
: "Add Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
IProjectIssuesFilterStore,
|
||||
IViewIssuesFilterStore,
|
||||
} from "store/issues";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
issuesFilterStore:
|
||||
@@ -41,7 +43,14 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
const { issuesFilterStore, issues, groupedIssueIds, layout, showWeekends, quickActions, quickAddCallback, viewId } =
|
||||
props;
|
||||
|
||||
const { calendar: calendarStore } = useMobxStore();
|
||||
const {
|
||||
calendar: calendarStore,
|
||||
projectIssues: issueStore,
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const { enableIssueCreation } = issueStore?.viewFlags || {};
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const calendarPayload = calendarStore.calendarPayload;
|
||||
|
||||
@@ -71,6 +80,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
@@ -85,6 +95,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Droppable } from "@hello-pangea/dnd";
|
||||
// components
|
||||
@@ -26,6 +27,7 @@ type Props = {
|
||||
groupedIssueIds: IGroupedIssues;
|
||||
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@@ -43,14 +45,16 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
groupedIssueIds,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
||||
|
||||
const issueIdList = groupedIssueIds ? groupedIssueIds[renderDateFormat(date.date)] : null;
|
||||
|
||||
const totalIssues = issueIdList?.length ?? 0;
|
||||
return (
|
||||
<>
|
||||
<div className="group relative flex h-full w-full flex-col bg-custom-background-90">
|
||||
@@ -85,8 +89,14 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<CalendarIssueBlocks issues={issues} issueIdList={issueIdList} quickActions={quickActions} />
|
||||
{enableQuickIssueCreate && (
|
||||
<CalendarIssueBlocks
|
||||
issues={issues}
|
||||
issueIdList={issueIdList}
|
||||
quickActions={quickActions}
|
||||
showAllIssues={showAllIssues}
|
||||
/>
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<div className="px-2 py-1">
|
||||
<CalendarQuickAddIssueForm
|
||||
formKey="target_date"
|
||||
@@ -96,9 +106,23 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
onOpen={() => setShowAllIssues(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totalIssues > 4 && (
|
||||
<div className="flex items-center px-2.5 py-1">
|
||||
<button
|
||||
type="button"
|
||||
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
|
||||
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,10 +15,11 @@ type Props = {
|
||||
issues: IIssueResponse | undefined;
|
||||
issueIdList: string[] | null;
|
||||
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
showAllIssues?: boolean;
|
||||
};
|
||||
|
||||
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
const { issues, issueIdList, quickActions } = props;
|
||||
const { issues, issueIdList, quickActions, showAllIssues = false } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
|
||||
@@ -52,7 +53,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueIdList?.map((issueId, index) => {
|
||||
{issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => {
|
||||
if (!issues?.[issueId]) return null;
|
||||
|
||||
const issue = issues?.[issueId];
|
||||
|
||||
@@ -27,6 +27,7 @@ type Props = {
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
onOpen?: () => void;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
@@ -57,7 +58,7 @@ const Inputs = (props: any) => {
|
||||
};
|
||||
|
||||
export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props;
|
||||
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId, onOpen } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
@@ -146,6 +147,11 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setIsOpen(true);
|
||||
if (onOpen) onOpen();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
@@ -169,7 +175,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100"
|
||||
onClick={() => setIsOpen(true)}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
|
||||
@@ -25,6 +25,7 @@ type Props = {
|
||||
week: ICalendarWeek | undefined;
|
||||
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@@ -42,6 +43,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
week,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
@@ -69,6 +71,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
quickActions={quickActions}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,8 @@ import emptyIssue from "public/empty-state/issue.svg";
|
||||
// types
|
||||
import { ISearchIssueResponse } from "types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
@@ -31,6 +33,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
cycleIssues: cycleIssueStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole: userRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -49,6 +52,8 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
@@ -75,10 +80,12 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
variant="neutral-primary"
|
||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
||||
onClick={() => setCycleIssuesListModal(true)}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
Add an existing issue
|
||||
</Button>
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -10,6 +10,8 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { ISearchIssueResponse } from "types";
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useState } from "react";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
@@ -26,6 +28,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
moduleIssues: moduleIssueStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole: userRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@@ -44,6 +47,8 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
@@ -70,10 +75,12 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
variant="neutral-primary"
|
||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
||||
onClick={() => setModuleIssuesListModal(true)}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
Add an existing issue
|
||||
</Button>
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { NewEmptyState } from "components/common/new-empty-state";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
// assets
|
||||
import emptyIssue from "public/empty-state/empty_issues.webp";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
@@ -12,8 +14,11 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
const {
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<NewEmptyState
|
||||
@@ -34,6 +39,7 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
},
|
||||
}}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import {
|
||||
AppliedDateFilters,
|
||||
@@ -16,6 +16,8 @@ import { X } from "lucide-react";
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: IIssueFilterOptions;
|
||||
@@ -33,10 +35,16 @@ const dateFilters = ["start_date", "target_date"];
|
||||
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, projects, states } = props;
|
||||
|
||||
const {
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
if (!appliedFilters) return null;
|
||||
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||
@@ -53,6 +61,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{membersFilters.includes(filterKey) && (
|
||||
<AppliedMembersFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
members={members}
|
||||
values={value}
|
||||
@@ -63,16 +72,22 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
{filterKey === "labels" && (
|
||||
<AppliedLabelsFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("labels", val)}
|
||||
labels={labels}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "priority" && (
|
||||
<AppliedPriorityFilters handleRemove={(val) => handleRemoveFilter("priority", val)} values={value} />
|
||||
<AppliedPriorityFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("priority", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "state" && states && (
|
||||
<AppliedStateFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("state", val)}
|
||||
states={states}
|
||||
values={value}
|
||||
@@ -86,30 +101,35 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
{filterKey === "project" && (
|
||||
<AppliedProjectFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("project", val)}
|
||||
projects={projects}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAllFilters}
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||
>
|
||||
Clear all
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAllFilters}
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||
>
|
||||
Clear all
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,10 +9,11 @@ type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
labels: IIssueLabel[] | undefined;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, labels, values } = props;
|
||||
const { handleRemove, labels, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -30,13 +31,15 @@ export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
/>
|
||||
<span className="normal-case">{labelDetails.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(labelId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(labelId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -9,10 +9,11 @@ type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
members: IUserLite[] | undefined;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, members, values } = props;
|
||||
const { handleRemove, members, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -25,13 +26,15 @@ export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
|
||||
<div key={memberId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<Avatar name={memberDetails.display_name} src={memberDetails.avatar} showTooltip={false} />
|
||||
<span className="normal-case">{memberDetails.display_name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(memberId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(memberId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -9,10 +9,11 @@ import { TIssuePriorities } from "types";
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, values } = props;
|
||||
const { handleRemove, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -20,13 +21,15 @@ export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
|
||||
<div key={priority} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<PriorityIcon priority={priority as TIssuePriorities} className={`h-3 w-3`} />
|
||||
{priority}
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(priority)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(priority)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -10,10 +10,11 @@ type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
projects: IProject[] | undefined;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, projects, values } = props;
|
||||
const { handleRemove, projects, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -34,13 +35,15 @@ export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
|
||||
</span>
|
||||
)}
|
||||
<span className="normal-case">{projectDetails.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(projectId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(projectId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -10,10 +10,11 @@ type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
states: IState[];
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, states, values } = props;
|
||||
const { handleRemove, states, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -26,13 +27,15 @@ export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
||||
<div key={stateId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<StateGroupIcon color={stateDetails.color} stateGroup={stateDetails.group} height="12px" width="12px" />
|
||||
{stateDetails.name}
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(stateId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(stateId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -11,10 +11,11 @@ type Props = {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
placement?: Placement;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
const { children, title = "Dropdown", placement } = props;
|
||||
const { children, title = "Dropdown", placement, disabled = false } = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -32,6 +33,7 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
<>
|
||||
<Popover.Button as={React.Fragment}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
ref={setReferenceElement}
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
|
||||
@@ -51,23 +51,23 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
|
||||
const issuesResponse = issueStore.getIssues;
|
||||
const issueIds = (issueStore.getIssuesIds ?? []) as TUnGroupedIssues;
|
||||
const { enableIssueCreation } = issueStore?.viewFlags || {};
|
||||
|
||||
const issues = issueIds.map((id) => issuesResponse?.[id]);
|
||||
|
||||
const updateIssue = async (issue: IIssue, payload: IBlockUpdateData) => {
|
||||
const updateIssueBlockStructure = async (issue: IIssue, data: IBlockUpdateData) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
//Todo fix sort order in the structure
|
||||
await issueStore.updateIssue(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
issue.id,
|
||||
{
|
||||
start_date: payload.start_date,
|
||||
target_date: payload.target_date,
|
||||
},
|
||||
viewId
|
||||
);
|
||||
const payload: any = { ...data };
|
||||
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
||||
|
||||
await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, payload, viewId);
|
||||
};
|
||||
|
||||
const updateIssue = async (projectId: string, issueId: string, payload: Partial<IIssue>) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issueStore.updateIssue(workspaceSlug.toString(), projectId, issueId, payload, viewId);
|
||||
};
|
||||
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
@@ -80,7 +80,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
blockUpdateHandler={updateIssue}
|
||||
blockUpdateHandler={updateIssueBlockStructure}
|
||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} />}
|
||||
sidebarToRender={(props) => (
|
||||
<IssueGanttSidebar
|
||||
@@ -88,6 +88,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
enableQuickIssueCreate
|
||||
disableIssueCreation={!enableIssueCreation || !isAllowed}
|
||||
/>
|
||||
)}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
@@ -102,8 +103,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
projectId={peekProjectId.toString()}
|
||||
issueId={peekIssueId.toString()}
|
||||
handleIssue={async (issueToUpdate) => {
|
||||
// TODO: update the logic here
|
||||
await updateIssue(issueToUpdate as IIssue, {});
|
||||
await updateIssue(peekProjectId.toString(), peekIssueId.toString(), issueToUpdate);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -7,9 +7,15 @@ import { useRouter } from "next/router";
|
||||
|
||||
export const CycleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query as { cycleId: string };
|
||||
const { cycleId } = router.query;
|
||||
|
||||
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
|
||||
|
||||
return <BaseGanttRoot issueFiltersStore={cycleIssueFilterStore} issueStore={cycleIssueStore} viewId={cycleId} />;
|
||||
return (
|
||||
<BaseGanttRoot
|
||||
issueFiltersStore={cycleIssueFilterStore}
|
||||
issueStore={cycleIssueStore}
|
||||
viewId={cycleId?.toString()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,9 +7,15 @@ import { useRouter } from "next/router";
|
||||
|
||||
export const ModuleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { moduleId } = router.query as { moduleId: string };
|
||||
const { moduleId } = router.query;
|
||||
|
||||
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
|
||||
|
||||
return <BaseGanttRoot issueFiltersStore={moduleIssueFilterStore} issueStore={moduleIssueStore} viewId={moduleId} />;
|
||||
return (
|
||||
<BaseGanttRoot
|
||||
issueFiltersStore={moduleIssueFilterStore}
|
||||
issueStore={moduleIssueStore}
|
||||
viewId={moduleId?.toString()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -195,25 +195,12 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
|
||||
const handleDeleteIssue = async () => {
|
||||
if (!handleDragDrop) return;
|
||||
await handleDragDrop(dragState.source, dragState.destination, sub_group_by, group_by, issues, issueIds)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: "Failed to delete issue",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
await handleDragDrop(dragState.source, dragState.destination, sub_group_by, group_by, issues, issueIds).finally(
|
||||
() => {
|
||||
setDeleteIssueModal(false);
|
||||
setDragState({});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||
@@ -343,7 +330,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
projects={workspaceProjects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
disableIssueCreation={true}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
enableQuickIssueCreate={enableQuickAdd}
|
||||
currentStore={currentStore}
|
||||
quickAddCallback={issueStore?.quickAddIssue}
|
||||
|
||||
@@ -149,7 +149,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
</Droppable>
|
||||
|
||||
<div className="sticky bottom-0 z-[0] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
||||
{enableQuickIssueCreate && (
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<KanBanQuickAddIssueForm
|
||||
formKey="name"
|
||||
groupId={getValueFromObject(_list, listKey) as string}
|
||||
|
||||
@@ -124,7 +124,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
disabled={isReadOnly}
|
||||
placeHolder="Start date"
|
||||
type="start_date"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -134,7 +134,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
disabled={isReadOnly}
|
||||
placeHolder="Target date"
|
||||
type="target_date"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{enableIssueQuickAdd && (
|
||||
{enableIssueQuickAdd && !disableIssueCreation && (
|
||||
<div className="sticky bottom-0 z-[1] w-full flex-shrink-0">
|
||||
<ListQuickAddIssueForm
|
||||
prePopulatedData={prePopulateQuickAddData(group_by, getValueFromObject(_list, listKey))}
|
||||
|
||||
@@ -108,7 +108,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
disabled={isReadonly}
|
||||
placeHolder="Start date"
|
||||
type="start_date"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -118,7 +118,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
disabled={isReadonly}
|
||||
placeHolder="Target date"
|
||||
type="target_date"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Check, ChevronDown, Search, User2 } from "lucide-react";
|
||||
import { Check, ChevronDown, CircleUser, Search } from "lucide-react";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
@@ -100,7 +100,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="Assignee" tooltipContent={getTooltipContent()} position="top">
|
||||
<div className="flex h-full w-full cursor-pointer items-center gap-2 text-custom-text-200">
|
||||
<div className="flex h-full w-full items-center gap-2 text-custom-text-200">
|
||||
{value && value.length > 0 && Array.isArray(value) ? (
|
||||
<AvatarGroup showTooltip={false}>
|
||||
{value.map((assigneeId) => {
|
||||
@@ -110,8 +110,8 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
})}
|
||||
</AvatarGroup>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
<span className="h-5 w-5 grid place-items-center">
|
||||
<CircleUser className="h-4 w-4" strokeWidth={1.5} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -140,7 +140,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex w-full items-center justify-between gap-1 text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => !projectMembers && getWorkspaceMembers()}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
// headless ui
|
||||
import { Popover } from "@headlessui/react";
|
||||
// lucide icons
|
||||
import { Calendar, X } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, X } from "lucide-react";
|
||||
// react date picker
|
||||
import DatePicker from "react-datepicker";
|
||||
// mobx
|
||||
@@ -18,11 +18,24 @@ export interface IIssuePropertyDate {
|
||||
value: any;
|
||||
onChange: (date: any) => void;
|
||||
disabled?: boolean;
|
||||
placeHolder?: string;
|
||||
type: "start_date" | "target_date";
|
||||
}
|
||||
|
||||
const DATE_OPTIONS = {
|
||||
start_date: {
|
||||
key: "start_date",
|
||||
placeholder: "Start date",
|
||||
icon: CalendarClock,
|
||||
},
|
||||
target_date: {
|
||||
key: "target_date",
|
||||
placeholder: "Target date",
|
||||
icon: CalendarCheck2,
|
||||
},
|
||||
};
|
||||
|
||||
export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props) => {
|
||||
const { value, onChange, disabled, placeHolder } = props;
|
||||
const { value, onChange, disabled, type } = props;
|
||||
|
||||
const dropdownBtn = React.useRef<any>(null);
|
||||
const dropdownOptions = React.useRef<any>(null);
|
||||
@@ -31,6 +44,8 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
const dateOptionDetails = DATE_OPTIONS[type];
|
||||
|
||||
return (
|
||||
<Popover as="div" className="relative">
|
||||
{({ open }) => {
|
||||
@@ -49,10 +64,10 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2 overflow-hidden">
|
||||
<Calendar className="h-3 w-3" strokeWidth={2} />
|
||||
<dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} />
|
||||
{value && (
|
||||
<>
|
||||
<Tooltip tooltipHeading={placeHolder} tooltipContent={value ?? "None"}>
|
||||
<Tooltip tooltipHeading={dateOptionDetails.placeholder} tooltipContent={value ?? "None"}>
|
||||
<div className="text-xs">{value}</div>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { usePopper } from "react-popper";
|
||||
// components
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { Check, ChevronDown, Search, Tags } from "lucide-react";
|
||||
// types
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { RootStore } from "store/root";
|
||||
@@ -25,6 +25,7 @@ export interface IIssuePropertyLabels {
|
||||
placement?: Placement;
|
||||
maxRender?: number;
|
||||
noLabelBorder?: boolean;
|
||||
placeholderText?: string;
|
||||
}
|
||||
|
||||
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
|
||||
@@ -41,6 +42,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
placement,
|
||||
maxRender = 2,
|
||||
noLabelBorder = false,
|
||||
placeholderText,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@@ -144,11 +146,12 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
className={`flex h-full items-center justify-center rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||
className={`h-full flex items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
|
||||
}`}
|
||||
>
|
||||
Select labels
|
||||
<Tags className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
{placeholderText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -171,8 +174,8 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: value.length <= maxRender
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => !storeLabels && fetchLabels()}
|
||||
>
|
||||
|
||||
@@ -94,7 +94,7 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="State" tooltipContent={selectedOption?.name ?? ""} position="top">
|
||||
<div className="flex w-full cursor-pointer items-center gap-2 text-custom-text-200">
|
||||
<div className="flex w-full items-center gap-2 text-custom-text-200">
|
||||
{selectedOption && <StateGroupIcon stateGroup={selectedOption?.group as any} color={selectedOption?.color} />}
|
||||
<span className="line-clamp-1 inline-block truncate">{selectedOption?.name ?? "State"}</span>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
@@ -75,7 +75,10 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setIssueToEdit({
|
||||
...issue,
|
||||
cycle: cycleId?.toString() ?? null,
|
||||
});
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
@@ -75,7 +75,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null });
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
@@ -12,6 +14,8 @@ import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
import { IIssue } from "types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
// constant
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, customActionButton } = props;
|
||||
@@ -24,6 +28,12 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(null);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
const { currentProjectRole } = userStore;
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
@@ -71,43 +81,47 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit issue
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Copy className="h-3 w-3" />
|
||||
Make a copy
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete issue
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && (
|
||||
<>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit issue
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Copy className="h-3 w-3" />
|
||||
Make a copy
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete issue
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
user: userStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const { enableInlineEditing, enableQuickAdd } = issueStore?.viewFlags || {};
|
||||
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
|
||||
|
||||
const { currentProjectRole } = userStore;
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
@@ -120,6 +120,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
enableQuickCreateIssue={enableQuickAdd}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IIssue, IUserLite } from "types";
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
members: IUserLite[] | undefined;
|
||||
onChange: (data: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@@ -18,7 +18,7 @@ type Props = {
|
||||
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -26,8 +26,13 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
|
||||
projectId={issue.project_detail?.id ?? null}
|
||||
value={issue.assignees}
|
||||
defaultOptions={issue?.assignee_details ? issue.assignee_details : []}
|
||||
onChange={(data) => onChange({ assignees: data })}
|
||||
className="h-full w-full"
|
||||
onChange={(data) => {
|
||||
onChange(issue, { assignees: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { assignees: data });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
|
||||
noLabelBorder
|
||||
hideDropdownArrow
|
||||
@@ -40,14 +45,16 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue) => (
|
||||
<SpreadsheetAssigneeColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
members={members}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetAssigneeColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
members={members}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center px-2.5 py-1 text-xs">
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issue.attachment_count} {issue.attachment_count === 1 ? "attachment" : "attachments"}
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,9 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetAttachmentColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetAttachmentColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center justify-center text-xs">
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{renderLongDetailDateFormat(issue.created_at)}
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,9 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetCreatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className="h-11">
|
||||
<SpreadsheetCreatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (data: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@@ -17,14 +17,19 @@ type Props = {
|
||||
export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
onChange={(val) => onChange({ target_date: val })}
|
||||
className="flex !h-full !w-full max-w-full items-center px-2.5 py-1"
|
||||
onChange={(val) => {
|
||||
onChange(issue, { target_date: val });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { target_date: val });
|
||||
}
|
||||
}}
|
||||
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
noBorder
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -34,13 +39,15 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, exp
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetDueDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetDueDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (formData: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@@ -17,15 +17,20 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePropertyEstimates
|
||||
projectId={issue.project_detail?.id ?? null}
|
||||
value={issue.estimate_point}
|
||||
onChange={(data) => onChange({ estimate_point: data })}
|
||||
className="h-full w-full"
|
||||
onChange={(data) => {
|
||||
onChange(issue, { estimate_point: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { estimate_point: data });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="h-full w-full px-2.5 py-1 !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={disabled}
|
||||
@@ -36,13 +41,15 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetEstimateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetEstimateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IIssue, IIssueLabel } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (formData: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
|
||||
labels: IIssueLabel[] | undefined;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
@@ -20,7 +20,7 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -28,13 +28,18 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||
projectId={issue.project_detail?.id ?? null}
|
||||
value={issue.labels}
|
||||
defaultOptions={issue?.label_details ? issue.label_details : []}
|
||||
onChange={(data) => onChange({ labels: data })}
|
||||
className="h-full w-full"
|
||||
onChange={(data) => {
|
||||
onChange(issue, { labels: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { assignees: data });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="px-2.5 h-full"
|
||||
noLabelBorder
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
disabled={disabled}
|
||||
placeholderText="Select labels"
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
@@ -42,14 +47,16 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetLabelColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
labels={labels}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetLabelColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
labels={labels}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center px-2.5 py-1 text-xs">
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issue.link_count} {issue.link_count === 1 ? "link" : "links"}
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,9 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetLinkColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetLinkColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (data: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@@ -17,15 +17,20 @@ type Props = {
|
||||
export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={(data) => onChange({ priority: data })}
|
||||
className="h-full w-full"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
|
||||
onChange={(data) => {
|
||||
onChange(issue, { priority: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { priority: data });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1"
|
||||
showTitle
|
||||
highlightUrgentPriority={false}
|
||||
hideDropdownArrow
|
||||
@@ -37,13 +42,15 @@ export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, ex
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetPriorityColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetPriorityColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (formData: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@@ -17,14 +17,19 @@ type Props = {
|
||||
export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
onChange={(val) => onChange({ start_date: val })}
|
||||
className="flex !h-full !w-full max-w-full items-center px-2.5 py-1"
|
||||
onChange={(val) => {
|
||||
onChange(issue, { start_date: val });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { start_date: val });
|
||||
}
|
||||
}}
|
||||
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
noBorder
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -34,13 +39,15 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, e
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetStartDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetStartDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,10 +6,12 @@ import { IssuePropertyState } from "../../properties";
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { IIssue, IState } from "types";
|
||||
import { mutate } from "swr";
|
||||
import { SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (data: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||
states: IState[] | undefined;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
@@ -20,7 +22,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -28,8 +30,13 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||
projectId={issue.project_detail?.id ?? null}
|
||||
value={issue.state}
|
||||
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
|
||||
onChange={(data) => onChange({ state: data.id, state_detail: data })}
|
||||
className="h-full w-full"
|
||||
onChange={(data) => {
|
||||
onChange(issue, { state: data.id, state_detail: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { state: data.id, state_detail: data });
|
||||
}
|
||||
}}
|
||||
className="w-full !h-11 border-b-[0.5px] border-custom-border-200"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full"
|
||||
hideDropdownArrow
|
||||
disabled={disabled}
|
||||
@@ -40,14 +47,16 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue) => (
|
||||
<SpreadsheetStateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
states={states}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="h-11">
|
||||
<SpreadsheetStateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
onChange={onChange}
|
||||
states={states}
|
||||
expandedIssues={expandedIssues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center px-2.5 py-1 text-xs">
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,9 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetSubIssueColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetSubIssueColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full w-full items-center justify-center text-xs">
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{renderLongDetailDateFormat(issue.updated_at)}
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,9 @@ export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetUpdatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
<div className={`h-11`}>
|
||||
<SpreadsheetUpdatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -163,18 +163,13 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
||||
{issues?.map((issue) => {
|
||||
const disableUserActions = !canEditProperties(issue.project);
|
||||
return (
|
||||
<div
|
||||
key={`${property}-${issue.id}`}
|
||||
className={`h-11 border-b-[0.5px] border-custom-border-200 ${
|
||||
disableUserActions ? "" : "cursor-pointer hover:bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<div key={`${property}-${issue.id}`} className={`h-fit ${disableUserActions ? "" : "cursor-pointer"}`}>
|
||||
{property === "state" ? (
|
||||
<SpreadsheetStateColumn
|
||||
disabled={disableUserActions}
|
||||
expandedIssues={expandedIssues}
|
||||
issue={issue}
|
||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
states={states}
|
||||
/>
|
||||
) : property === "priority" ? (
|
||||
@@ -182,14 +177,14 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
||||
disabled={disableUserActions}
|
||||
expandedIssues={expandedIssues}
|
||||
issue={issue}
|
||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
/>
|
||||
) : property === "estimate" ? (
|
||||
<SpreadsheetEstimateColumn
|
||||
disabled={disableUserActions}
|
||||
expandedIssues={expandedIssues}
|
||||
issue={issue}
|
||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
/>
|
||||
) : property === "assignee" ? (
|
||||
<SpreadsheetAssigneeColumn
|
||||
@@ -197,7 +192,7 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
||||
expandedIssues={expandedIssues}
|
||||
issue={issue}
|
||||
members={members}
|
||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
/>
|
||||
) : property === "labels" ? (
|
||||
<SpreadsheetLabelColumn
|
||||
@@ -205,21 +200,21 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
||||
expandedIssues={expandedIssues}
|
||||
issue={issue}
|
||||
labels={labels}
|
||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
/>
|
||||
) : property === "start_date" ? (
|
||||
<SpreadsheetStartDateColumn
|
||||
disabled={disableUserActions}
|
||||
expandedIssues={expandedIssues}
|
||||
issue={issue}
|
||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
/>
|
||||
) : property === "due_date" ? (
|
||||
<SpreadsheetDueDateColumn
|
||||
disabled={disableUserActions}
|
||||
expandedIssues={expandedIssues}
|
||||
issue={issue}
|
||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||
/>
|
||||
) : property === "created_on" ? (
|
||||
<SpreadsheetCreatedOnColumn expandedIssues={expandedIssues} issue={issue} />
|
||||
|
||||
@@ -33,6 +33,7 @@ type Props = {
|
||||
viewId?: string;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
enableQuickCreateIssue?: boolean;
|
||||
disableIssueCreation?: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
@@ -50,6 +51,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
viewId,
|
||||
canEditProperties,
|
||||
enableQuickCreateIssue,
|
||||
disableIssueCreation,
|
||||
} = props;
|
||||
// states
|
||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||
@@ -77,6 +79,13 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!issues || issues.length === 0)
|
||||
return (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full overflow-x-auto whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
|
||||
<div className="flex h-full w-full flex-col">
|
||||
@@ -84,7 +93,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
ref={containerRef}
|
||||
className="horizontal-scroll-enable flex divide-x-[0.5px] divide-custom-border-200 overflow-y-auto"
|
||||
>
|
||||
{issues && issues.length > 0 ? (
|
||||
{issues && issues.length > 0 && (
|
||||
<>
|
||||
<div className="sticky left-0 z-[2] w-[28rem]">
|
||||
<div
|
||||
@@ -134,17 +143,13 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
states={states}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div /> {/* empty div to show right most border */}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-custom-border-100">
|
||||
<div className="z-5 sticky bottom-0 left-0 mb-3">
|
||||
{enableQuickCreateIssue && (
|
||||
{enableQuickCreateIssue && !disableIssueCreation && (
|
||||
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -216,7 +216,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
</CustomMenu>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mb-5 flex items-center">
|
||||
<div className="mb-2.5 flex items-center">
|
||||
{currentIssueState && (
|
||||
<StateGroupIcon
|
||||
className="mr-3 h-4 w-4"
|
||||
|
||||
@@ -266,11 +266,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be created. Please try again.",
|
||||
message: err.detail ?? "Issue could not be created. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"ISSUE_CREATED",
|
||||
@@ -312,11 +312,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
|
||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be created. Please try again.",
|
||||
message: err.detail ?? "Issue could not be created. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -347,11 +347,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be updated. Please try again.",
|
||||
message: err.detail ?? "Issue could not be updated. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"ISSUE_UPDATED",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, useState } from "react";
|
||||
import { mutate } from "swr";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
@@ -17,30 +17,25 @@ import {
|
||||
SidebarPrioritySelect,
|
||||
SidebarStateSelect,
|
||||
} from "../sidebar-select";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { LinkModal, LinksList } from "components/core";
|
||||
// types
|
||||
import { IIssue, IIssueLink, TIssuePriorities, ILinkDetails } from "types";
|
||||
// fetch-keys
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
import { IIssue, TIssuePriorities, ILinkDetails, IIssueLink } from "types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
interface IPeekOverviewProperties {
|
||||
issue: IIssue;
|
||||
issueUpdate: (issue: Partial<IIssue>) => void;
|
||||
issueLinkCreate: (data: IIssueLink) => Promise<ILinkDetails>;
|
||||
issueLinkUpdate: (data: IIssueLink, linkId: string) => Promise<ILinkDetails>;
|
||||
issueLinkDelete: (linkId: string) => Promise<void>;
|
||||
disableUserActions: boolean;
|
||||
}
|
||||
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((props) => {
|
||||
const { issue, issueUpdate, disableUserActions } = props;
|
||||
const { issue, issueUpdate, issueLinkCreate, issueLinkUpdate, issueLinkDelete, disableUserActions } = props;
|
||||
// states
|
||||
const [linkModal, setLinkModal] = useState(false);
|
||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
|
||||
@@ -54,8 +49,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleState = (_state: string) => {
|
||||
issueUpdate({ ...issue, state: _state });
|
||||
};
|
||||
@@ -81,61 +74,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
issueUpdate({ ...issue, ...formData });
|
||||
};
|
||||
|
||||
const handleCreateLink = async (formData: IIssueLink) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { metadata: {}, ...formData };
|
||||
|
||||
await issueService
|
||||
.createIssueLink(workspaceSlug as string, projectId as string, issue.id, payload)
|
||||
.then(() => mutate(ISSUE_DETAILS(issue.id)))
|
||||
.catch((err) => {
|
||||
if (err.status === 400)
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "This URL already exists for this issue.",
|
||||
});
|
||||
else
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateLink = async (formData: IIssueLink, linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { metadata: {}, ...formData };
|
||||
|
||||
const updatedLinks = issue.issue_link.map((l) =>
|
||||
l.id === linkId
|
||||
? {
|
||||
...l,
|
||||
title: formData.title,
|
||||
url: formData.url,
|
||||
}
|
||||
: l
|
||||
);
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issue.id),
|
||||
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
||||
false
|
||||
);
|
||||
|
||||
await issueService
|
||||
.updateIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId, payload)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issue.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCycleOrModuleChange = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
@@ -147,27 +85,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
setLinkModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteLink = async (linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const updatedLinks = issue.issue_link.filter((l) => l.id !== linkId);
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issue.id),
|
||||
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
||||
false
|
||||
);
|
||||
|
||||
await issueService
|
||||
.deleteIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issue.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
const projectDetails = workspaceSlug ? getProjectById(workspaceSlug.toString(), issue.project) : null;
|
||||
const isEstimateEnabled = projectDetails?.estimate;
|
||||
|
||||
@@ -187,8 +104,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
}}
|
||||
data={selectedLinkToUpdate}
|
||||
status={selectedLinkToUpdate ? true : false}
|
||||
createIssueLink={handleCreateLink}
|
||||
updateIssueLink={handleUpdateLink}
|
||||
createIssueLink={issueLinkCreate}
|
||||
updateIssueLink={issueLinkUpdate}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full flex-col gap-5 py-5">
|
||||
@@ -373,7 +290,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
{issue?.issue_link && issue.issue_link.length > 0 ? (
|
||||
<LinksList
|
||||
links={issue.issue_link}
|
||||
handleDeleteLink={handleDeleteLink}
|
||||
handleDeleteLink={issueLinkDelete}
|
||||
handleEditLink={handleEditLink}
|
||||
userAuth={{
|
||||
isGuest: currentProjectRole === EUserWorkspaceRoles.GUEST,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IssueView } from "components/issues";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IIssue, IIssueLink } from "types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
@@ -42,6 +42,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
removeIssueReaction,
|
||||
createIssueSubscription,
|
||||
removeIssueSubscription,
|
||||
createIssueLink,
|
||||
updateIssueLink,
|
||||
deleteIssueLink,
|
||||
getIssue,
|
||||
loader,
|
||||
fetchPeekIssueDetails,
|
||||
@@ -121,6 +124,13 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
|
||||
const issueSubscriptionRemove = () => removeIssueSubscription(workspaceSlug, projectId, issueId);
|
||||
|
||||
const issueLinkCreate = (formData: IIssueLink) => createIssueLink(workspaceSlug, projectId, issueId, formData);
|
||||
|
||||
const issueLinkUpdate = (formData: IIssueLink, linkId: string) =>
|
||||
updateIssueLink(workspaceSlug, projectId, issueId, linkId, formData);
|
||||
|
||||
const issueLinkDelete = (linkId: string) => deleteIssueLink(workspaceSlug, projectId, issueId, linkId);
|
||||
|
||||
const handleDeleteIssue = async () => {
|
||||
if (isArchived) await deleteArchivedIssue(workspaceSlug, projectId, issue!);
|
||||
else removeIssueFromStructure(workspaceSlug, projectId, issue!);
|
||||
@@ -159,6 +169,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
issueSubscriptionCreate={issueSubscriptionCreate}
|
||||
issueSubscriptionRemove={issueSubscriptionRemove}
|
||||
issueLinkCreate={issueLinkCreate}
|
||||
issueLinkUpdate={issueLinkUpdate}
|
||||
issueLinkDelete={issueLinkDelete}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
disableUserActions={[5, 10].includes(userRole)}
|
||||
showCommentAccessSpecifier={currentProjectDetails?.is_deployed}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
// ui
|
||||
import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IIssue, IIssueLink, ILinkDetails } from "types";
|
||||
|
||||
interface IIssueView {
|
||||
workspaceSlug: string;
|
||||
@@ -38,6 +38,9 @@ interface IIssueView {
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
issueSubscriptionCreate: () => void;
|
||||
issueSubscriptionRemove: () => void;
|
||||
issueLinkCreate: (formData: IIssueLink) => Promise<ILinkDetails>;
|
||||
issueLinkUpdate: (formData: IIssueLink, linkId: string) => Promise<ILinkDetails>;
|
||||
issueLinkDelete: (linkId: string) => Promise<void>;
|
||||
handleDeleteIssue: () => Promise<void>;
|
||||
children: ReactNode;
|
||||
disableUserActions?: boolean;
|
||||
@@ -84,6 +87,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
issueCommentReactionRemove,
|
||||
issueSubscriptionCreate,
|
||||
issueSubscriptionRemove,
|
||||
issueLinkCreate,
|
||||
issueLinkUpdate,
|
||||
issueLinkDelete,
|
||||
handleDeleteIssue,
|
||||
children,
|
||||
disableUserActions = false,
|
||||
@@ -286,6 +292,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
<PeekOverviewProperties
|
||||
issue={issue}
|
||||
issueUpdate={issueUpdate}
|
||||
issueLinkCreate={issueLinkCreate}
|
||||
issueLinkUpdate={issueLinkUpdate}
|
||||
issueLinkDelete={issueLinkDelete}
|
||||
disableUserActions={disableUserActions}
|
||||
/>
|
||||
|
||||
@@ -342,6 +351,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
<PeekOverviewProperties
|
||||
issue={issue}
|
||||
issueUpdate={issueUpdate}
|
||||
issueLinkCreate={issueLinkCreate}
|
||||
issueLinkUpdate={issueLinkUpdate}
|
||||
issueLinkDelete={issueLinkDelete}
|
||||
disableUserActions={disableUserActions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import type { FieldError } from "react-hook-form";
|
||||
// mobx store
|
||||
@@ -23,9 +22,6 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = observer((p
|
||||
const { value, onChange } = props;
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -33,13 +29,13 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = observer((p
|
||||
placement: "bottom-start",
|
||||
});
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
const {
|
||||
project: { joinedProjects },
|
||||
} = useMobxStore();
|
||||
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||
const selectedProject = joinedProjects?.find((i) => i.id === value);
|
||||
|
||||
const selectedProject = projects?.find((i) => i.id === value);
|
||||
|
||||
const options = projects?.map((project) => ({
|
||||
const options = joinedProjects?.map((project) => ({
|
||||
value: project.id,
|
||||
query: project.name,
|
||||
content: (
|
||||
@@ -61,8 +57,8 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = observer((p
|
||||
{selectedProject.emoji
|
||||
? renderEmoji(selectedProject.emoji)
|
||||
: selectedProject.icon_prop
|
||||
? renderEmoji(selectedProject.icon_prop)
|
||||
: null}
|
||||
? renderEmoji(selectedProject.icon_prop)
|
||||
: null}
|
||||
</span>
|
||||
<div className="truncate">{selectedProject.identifier}</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user