Merge pull request #3175 from makeplane/preview

release: moving changes from preview to master
This commit is contained in:
sriram veeraghanta 2023-12-18 19:29:43 +05:30 committed by GitHub
commit e7468292c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
195 changed files with 1962 additions and 2487 deletions

View File

@ -1,11 +1,13 @@
name: Create PR in Plane EE Repository to sync the changes name: Create Sync Action
on: on:
pull_request: pull_request:
branches: branches:
- master - preview
types: types:
- closed - closed
env:
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
jobs: jobs:
create_pr: create_pr:
@ -16,27 +18,13 @@ jobs:
pull-requests: write pull-requests: write
contents: read contents: read
steps: steps:
- name: Check SOURCE_REPO
id: check_repo
env:
SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }}
run: |
echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)"
- name: Checkout Code - name: Checkout Code
if: steps.check_repo.outputs.is_correct_repo == 'true'
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
persist-credentials: false persist-credentials: false
fetch-depth: 0 fetch-depth: 0
- name: Set up Branch Name
if: steps.check_repo.outputs.is_correct_repo == 'true'
run: |
echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
- name: Setup GH CLI - name: Setup GH CLI
if: steps.check_repo.outputs.is_correct_repo == 'true'
run: | run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
@ -45,35 +33,14 @@ jobs:
sudo apt update sudo apt update
sudo apt install gh -y sudo apt install gh -y
- name: Create Pull Request - name: Push Changes to Target Repo
if: steps.check_repo.outputs.is_correct_repo == 'true'
env: env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: | run: |
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}" TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}" TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH git checkout $SOURCE_BRANCH
git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git" git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target $SOURCE_BRANCH:$SOURCE_BRANCH git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"
# Remove double quotes
PR_TITLE_CLEANED="${PR_TITLE//\"/}"
PR_BODY_CLEANED="${PR_BODY//\"/}"
# Construct PR_BODY_CONTENT using a here-document
PR_BODY_CONTENT=$(cat <<EOF
$PR_BODY_CLEANED
EOF
)
gh pr create \
--base $TARGET_BRANCH \
--head $SOURCE_BRANCH \
--title "[SYNC] $PR_TITLE_CLEANED" \
--body "$PR_BODY_CONTENT" \
--repo $TARGET_REPO

View File

@ -33,8 +33,8 @@ The backend is a django project which is kept inside apiserver
1. Clone the repo 1. Clone the repo
```bash ```bash
git clone https://github.com/makeplane/plane git clone https://github.com/makeplane/plane.git [folder-name]
cd plane cd [folder-name]
chmod +x setup.sh chmod +x setup.sh
``` ```
@ -44,33 +44,12 @@ chmod +x setup.sh
./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 ```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? ## Missing a Feature?

View File

@ -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. > 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 ## ⚡️ Contributors Quick Start
@ -63,7 +63,7 @@ Thats it!
## 🍙 Self Hosting ## 🍙 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 ## 🚀 Features

View File

@ -49,5 +49,5 @@ USER captain
# Expose container port and run entry point script # Expose container port and run entry point script
EXPOSE 8000 EXPOSE 8000
# CMD [ "./bin/takeoff" ] CMD [ "./bin/takeoff.local" ]

31
apiserver/bin/takeoff.local Executable file
View File

@ -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

View File

@ -145,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() .distinct()
) )
@ -160,16 +170,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
projects = ( projects = (
self.get_queryset() self.get_queryset()
.annotate(sort_order=Subquery(sort_order_query)) .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") .order_by("sort_order", "name")
) )
if request.GET.get("per_page", False) and request.GET.get("cursor", False): if request.GET.get("per_page", False) and request.GET.get("cursor", False):
@ -679,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( project_members = ProjectMember.objects.bulk_create(
bulk_project_members, bulk_project_members,
batch_size=10, batch_size=10,
@ -991,11 +1010,18 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
def get(self, request): def get(self, request):
files = [] files = []
s3 = boto3.client( s3_client_params = {
"s3", "service_name": "s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID, "aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, "aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
) }
# Use AWS_S3_ENDPOINT_URL if it is present in the settings
if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL:
s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
s3 = boto3.client(**s3_client_params)
params = { params = {
"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Prefix": "static/project-cover/", "Prefix": "static/project-cover/",
@ -1008,9 +1034,19 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
if not content["Key"].endswith( if not content["Key"].endswith(
"/" "/"
): # This line ensures we're only getting files, not "sub-folders" ): # This line ensures we're only getting files, not "sub-folders"
files.append( if (
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
) and settings.AWS_S3_CUSTOM_DOMAIN
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
and settings.AWS_S3_URL_PROTOCOL
):
files.append(
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
)
else:
files.append(
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
)
return Response(files, status=status.HTTP_200_OK) return Response(files, status=status.HTTP_200_OK)

View File

@ -70,6 +70,7 @@ from plane.app.permissions import (
WorkSpaceAdminPermission, WorkSpaceAdminPermission,
WorkspaceEntityPermission, WorkspaceEntityPermission,
WorkspaceViewerPermission, WorkspaceViewerPermission,
WorkspaceUserPermission,
) )
from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
@ -495,6 +496,18 @@ class WorkSpaceMemberViewSet(BaseViewSet):
WorkspaceEntityPermission, 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 = [ search_fields = [
"member__display_name", "member__display_name",
"member__first_name", "member__first_name",

View File

@ -65,7 +65,7 @@ def send_export_email(email, slug, csv_buffer, rows):
port=int(EMAIL_PORT), port=int(EMAIL_PORT),
username=EMAIL_HOST_USER, username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD, password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS), use_tls=EMAIL_USE_TLS == "1",
) )
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(

View File

@ -51,7 +51,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
port=int(EMAIL_PORT), port=int(EMAIL_PORT),
username=EMAIL_HOST_USER, username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD, password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS), use_tls=EMAIL_USE_TLS == "1",
) )
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(

View File

@ -41,7 +41,7 @@ def magic_link(email, key, token, current_site):
port=int(EMAIL_PORT), port=int(EMAIL_PORT),
username=EMAIL_HOST_USER, username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD, password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS), use_tls=EMAIL_USE_TLS == "1",
) )
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(

View File

@ -60,7 +60,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
port=int(EMAIL_PORT), port=int(EMAIL_PORT),
username=EMAIL_HOST_USER, username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD, password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS), use_tls=EMAIL_USE_TLS == "1",
) )
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(

View File

@ -70,7 +70,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
port=int(EMAIL_PORT), port=int(EMAIL_PORT),
username=EMAIL_HOST_USER, username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD, password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS), use_tls=EMAIL_USE_TLS == "1",
) )
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(

View File

@ -30,7 +30,7 @@ openpyxl==3.1.2
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
dj-database-url==2.1.0 dj-database-url==2.1.0
posthog==3.0.2 posthog==3.0.2
cryptography==41.0.5 cryptography==41.0.6
lxml==4.9.3 lxml==4.9.3
boto3==1.28.40 boto3==1.28.40

View File

@ -39,7 +39,7 @@ function download(){
echo "" echo ""
echo "Latest version is now available for you to use" echo "Latest version is now available for you to use"
echo "" echo ""
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file." echo "In case of Upgrade, your new setting file is available as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
echo "" echo ""
} }

View File

@ -12,7 +12,6 @@ volumes:
services: services:
plane-redis: plane-redis:
container_name: plane-redis
image: redis:6.2.7-alpine image: redis:6.2.7-alpine
restart: unless-stopped restart: unless-stopped
networks: networks:
@ -21,7 +20,6 @@ services:
- redisdata:/data - redisdata:/data
plane-minio: plane-minio:
container_name: plane-minio
image: minio/minio image: minio/minio
restart: unless-stopped restart: unless-stopped
networks: networks:
@ -36,7 +34,6 @@ services:
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
plane-db: plane-db:
container_name: plane-db
image: postgres:15.2-alpine image: postgres:15.2-alpine
restart: unless-stopped restart: unless-stopped
networks: networks:
@ -53,7 +50,6 @@ services:
PGDATA: /var/lib/postgresql/data PGDATA: /var/lib/postgresql/data
web: web:
container_name: web
build: build:
context: . context: .
dockerfile: ./web/Dockerfile.dev dockerfile: ./web/Dockerfile.dev
@ -61,8 +57,7 @@ services:
networks: networks:
- dev_env - dev_env
volumes: volumes:
- .:/app - ./web:/app/web
command: yarn dev --filter=web
env_file: env_file:
- ./web/.env - ./web/.env
depends_on: depends_on:
@ -73,22 +68,17 @@ services:
build: build:
context: . context: .
dockerfile: ./space/Dockerfile.dev dockerfile: ./space/Dockerfile.dev
container_name: space
restart: unless-stopped restart: unless-stopped
networks: networks:
- dev_env - dev_env
volumes: volumes:
- .:/app - ./space:/app/space
command: yarn dev --filter=space
env_file:
- ./space/.env
depends_on: depends_on:
- api - api
- worker - worker
- web - web
api: api:
container_name: api
build: build:
context: ./apiserver context: ./apiserver
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
@ -99,7 +89,7 @@ services:
- dev_env - dev_env
volumes: volumes:
- ./apiserver:/code - ./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: env_file:
- ./apiserver/.env - ./apiserver/.env
depends_on: depends_on:
@ -107,7 +97,6 @@ services:
- plane-redis - plane-redis
worker: worker:
container_name: bgworker
build: build:
context: ./apiserver context: ./apiserver
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
@ -127,7 +116,6 @@ services:
- plane-redis - plane-redis
beat-worker: beat-worker:
container_name: beatworker
build: build:
context: ./apiserver context: ./apiserver
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
@ -147,10 +135,9 @@ services:
- plane-redis - plane-redis
proxy: proxy:
container_name: proxy
build: build:
context: ./nginx context: ./nginx
dockerfile: Dockerfile dockerfile: Dockerfile.dev
restart: unless-stopped restart: unless-stopped
networks: networks:
- dev_env - dev_env

10
nginx/Dockerfile.dev Normal file
View File

@ -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"]

View File

@ -18,7 +18,7 @@ server {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
} }
location /space/ { location /spaces/ {
proxy_pass http://localhost:4000/; proxy_pass http://localhost:4000/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;

36
nginx/nginx.conf.dev Normal file
View File

@ -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/;
}
}
}

View File

@ -6,7 +6,6 @@ export LC_ALL=C
export LC_CTYPE=C export LC_CTYPE=C
cp ./web/.env.example ./web/.env cp ./web/.env.example ./web/.env
cp ./space/.env.example ./space/.env
cp ./apiserver/.env.example ./apiserver/.env cp ./apiserver/.env.example ./apiserver/.env
# Generate the SECRET_KEY that will be used by django # Generate the SECRET_KEY that will be used by django

View File

@ -7,5 +7,8 @@ WORKDIR /app
COPY . . COPY . .
RUN yarn global add turbo RUN yarn global add turbo
RUN yarn install 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"] CMD ["yarn","dev", "--filter=space"]

View File

@ -8,4 +8,5 @@ COPY . .
RUN yarn global add turbo RUN yarn global add turbo
RUN yarn install RUN yarn install
EXPOSE 3000 EXPOSE 3000
VOLUME [ "/app/node_modules", "/app/web/node_modules" ]
CMD ["yarn", "dev", "--filter=web"] CMD ["yarn", "dev", "--filter=web"]

View File

@ -19,7 +19,7 @@ type Props = {
icon?: any; icon?: any;
text: string; text: string;
onClick: () => void; onClick: () => void;
}; } | null;
disabled?: boolean; disabled?: boolean;
}; };

View File

@ -1,14 +1,14 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import { observer } from "mobx-react-lite";
// react hook form
import { SubmitHandler, useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Combobox, Dialog, Transition } from "@headlessui/react";
import useSWR from "swr";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast";
// services // services
import { IssueService } from "services/issue"; import { IssueService } from "services/issue";
// hooks
import useToast from "hooks/use-toast";
// ui // ui
import { Button, LayersIcon } from "@plane/ui"; import { Button, LayersIcon } from "@plane/ui";
// icons // icons
@ -30,17 +30,25 @@ type Props = {
const issueService = new IssueService(); const issueService = new IssueService();
export const BulkDeleteIssuesModal: React.FC<Props> = (props) => { export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
const { isOpen, onClose } = props; const { isOpen, onClose } = props;
// states
const [query, setQuery] = useState("");
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// states // store hooks
const [query, setQuery] = useState(""); const {
user: { hasPermissionToCurrentProject },
} = useMobxStore();
// fetching project issues. // fetching project issues.
const { data: issues } = useSWR( const { data: issues } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null, workspaceSlug && projectId && hasPermissionToCurrentProject
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null ? PROJECT_ISSUES_LIST(workspaceSlug.toString(), projectId.toString())
: null,
workspaceSlug && projectId && hasPermissionToCurrentProject
? () => issueService.getIssues(workspaceSlug.toString(), projectId.toString())
: null
); );
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -222,4 +230,4 @@ export const BulkDeleteIssuesModal: React.FC<Props> = (props) => {
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
); );
}; });

View File

@ -118,6 +118,7 @@ export const LinkModal: FC<Props> = (props) => {
ref={ref} ref={ref}
hasError={Boolean(errors.url)} hasError={Boolean(errors.url)}
placeholder="https://..." placeholder="https://..."
pattern="^(https?://).*"
className="w-full" className="w-full"
/> />
)} )}

View File

@ -50,8 +50,8 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEdit
</Tooltip> </Tooltip>
</div> </div>
{!isNotAllowed && ( <div className="z-[1] flex flex-shrink-0 items-center gap-2">
<div className="z-[1] flex flex-shrink-0 items-center gap-2"> {!isNotAllowed && (
<button <button
type="button" type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80" className="flex items-center justify-center p-1 hover:bg-custom-background-80"
@ -63,14 +63,16 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEdit
> >
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" /> <Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</button> </button>
<a )}
href={link.url} <a
target="_blank" href={link.url}
rel="noopener noreferrer" target="_blank"
className="flex items-center justify-center p-1 hover:bg-custom-background-80" rel="noopener noreferrer"
> className="flex items-center justify-center p-1 hover:bg-custom-background-80"
<ExternalLinkIcon className="h-3 w-3 stroke-[1.5] text-custom-text-200" /> >
</a> <ExternalLinkIcon className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</a>
{!isNotAllowed && (
<button <button
type="button" type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80" className="flex items-center justify-center p-1 hover:bg-custom-background-80"
@ -82,8 +84,8 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEdit
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</button> </button>
</div> )}
)} </div>
</div> </div>
<div className="px-5"> <div className="px-5">
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300"> <p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">

View File

@ -14,6 +14,7 @@ import { SingleProgressStats } from "components/core";
import { Avatar, StateGroupIcon } from "@plane/ui"; import { Avatar, StateGroupIcon } from "@plane/ui";
// types // types
import { import {
IIssueFilterOptions,
IModule, IModule,
TAssigneesDistribution, TAssigneesDistribution,
TCompletionChartDistribution, TCompletionChartDistribution,
@ -35,6 +36,9 @@ type Props = {
roundedTab?: boolean; roundedTab?: boolean;
noBackground?: boolean; noBackground?: boolean;
isPeekView?: boolean; isPeekView?: boolean;
isCompleted?: boolean;
filters?: IIssueFilterOptions;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
}; };
export const SidebarProgressStats: React.FC<Props> = ({ export const SidebarProgressStats: React.FC<Props> = ({
@ -44,7 +48,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
module, module,
roundedTab, roundedTab,
noBackground, noBackground,
isCompleted = false,
isPeekView = false, isPeekView = false,
filters,
handleFiltersUpdate,
}) => { }) => {
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees"); const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
@ -140,20 +147,11 @@ export const SidebarProgressStats: React.FC<Props> = ({
} }
completed={assignee.completed_issues} completed={assignee.completed_issues}
total={assignee.total_issues} total={assignee.total_issues}
{...(!isPeekView && { {...(!isPeekView &&
onClick: () => { !isCompleted && {
// TODO: set filters here onClick: () => handleFiltersUpdate("assignees", assignee.assignee_id ?? ""),
// if (filters?.assignees?.includes(assignee.assignee_id ?? "")) selected: filters?.assignees?.includes(assignee.assignee_id ?? ""),
// setFilters({ })}
// assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
// });
// else
// setFilters({
// assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
// });
},
// selected: filters?.assignees?.includes(assignee.assignee_id ?? ""),
})}
/> />
); );
else else
@ -200,17 +198,11 @@ export const SidebarProgressStats: React.FC<Props> = ({
} }
completed={label.completed_issues} completed={label.completed_issues}
total={label.total_issues} total={label.total_issues}
{...(!isPeekView && { {...(!isPeekView &&
// TODO: set filters here !isCompleted && {
onClick: () => { onClick: () => handleFiltersUpdate("labels", label.label_id ?? ""),
// if (filters.labels?.includes(label.label_id ?? "")) selected: filters?.labels?.includes(label.label_id ?? `no-label-${index}`),
// setFilters({ })}
// labels: filters?.labels?.filter((l) => l !== label.label_id),
// });
// else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
},
// selected: filters?.labels?.includes(label.label_id ?? ""),
})}
/> />
)) ))
) : ( ) : (

View File

@ -75,7 +75,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
useSWR( const { isLoading } = useSWR(
workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null,
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null
); );
@ -94,7 +94,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
// : null // : null
// ) as { data: IIssue[] | undefined }; // ) as { data: IIssue[] | undefined };
if (!cycle) if (!cycle && isLoading)
return ( return (
<Loader> <Loader>
<Loader.Item height="250px" /> <Loader.Item height="250px" />
@ -187,12 +187,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cycleStatus === "current" cycleStatus === "current"
? "#09A953" ? "#09A953"
: cycleStatus === "upcoming" : cycleStatus === "upcoming"
? "#F7AE59" ? "#F7AE59"
: cycleStatus === "completed" : cycleStatus === "completed"
? "#3F76FF" ? "#3F76FF"
: cycleStatus === "draft" : cycleStatus === "draft"
? "rgb(var(--color-text-200))" ? "rgb(var(--color-text-200))"
: "" : ""
}`} }`}
/> />
</span> </span>
@ -207,12 +207,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cycleStatus === "current" cycleStatus === "current"
? "bg-green-600/5 text-green-600" ? "bg-green-600/5 text-green-600"
: cycleStatus === "upcoming" : cycleStatus === "upcoming"
? "bg-orange-300/5 text-orange-300" ? "bg-orange-300/5 text-orange-300"
: cycleStatus === "completed" : cycleStatus === "completed"
? "bg-blue-500/5 text-blue-500" ? "bg-blue-500/5 text-blue-500"
: cycleStatus === "draft" : cycleStatus === "draft"
? "bg-neutral-400/5 text-neutral-400" ? "bg-neutral-400/5 text-neutral-400"
: "" : ""
}`} }`}
> >
{cycleStatus === "current" ? ( {cycleStatus === "current" ? (

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -29,7 +29,10 @@ import {
renderShortMonthDate, renderShortMonthDate,
} from "helpers/date-time.helper"; } from "helpers/date-time.helper";
// types // types
import { ICycle } from "types"; import { ICycle, IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// fetch-keys // fetch-keys
import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_STATUS } from "constants/cycle";
@ -52,7 +55,9 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { const {
cycle: cycleDetailsStore, cycle: cycleDetailsStore,
cycleIssuesFilter: { issueFilters, updateFilters },
trackEvent: { setTrackElement }, trackEvent: { setTrackElement },
user: { currentProjectRole },
} = useMobxStore(); } = useMobxStore();
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined; const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
@ -242,6 +247,25 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
} }
}; };
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId);
},
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
);
const cycleStatus = const cycleStatus =
cycleDetails?.start_date && cycleDetails?.end_date cycleDetails?.start_date && cycleDetails?.end_date
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
@ -270,10 +294,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</Loader> </Loader>
); );
const endDate = new Date(cycleDetails.end_date ?? ""); const endDate = new Date(watch("end_date") ?? cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_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); const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
@ -286,6 +311,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
: `${cycleDetails.total_issues}` : `${cycleDetails.total_issues}`
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<> <>
{cycleDetails && workspaceSlug && projectId && ( {cycleDetails && workspaceSlug && projectId && (
@ -312,7 +339,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<button onClick={handleCopyText}> <button onClick={handleCopyText}>
<LinkIcon className="h-3 w-3 text-custom-text-300" /> <LinkIcon className="h-3 w-3 text-custom-text-300" />
</button> </button>
{!isCompleted && ( {!isCompleted && isEditingAllowed && (
<CustomMenu width="lg" placement="bottom-end" ellipsis> <CustomMenu width="lg" placement="bottom-end" ellipsis>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => { onClick={() => {
@ -349,8 +376,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="relative flex h-full w-52 items-center gap-2.5"> <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 className="flex h-full items-center justify-center rounded-lg">
<Popover.Button <Popover.Button
disabled={isCompleted ?? false} className={`text-sm font-medium text-custom-text-300 ${
className="cursor-default text-sm font-medium text-custom-text-300" isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
> >
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
</Popover.Button> </Popover.Button>
@ -373,10 +402,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
handleStartDateChange(val); handleStartDateChange(val);
} }
}} }}
startDate={watch("start_date") ? `${watch("start_date")}` : null} startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ? `${watch("end_date")}` : null} endDate={watch("end_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("end_date")}`)} maxDate={new Date(`${watch("end_date")}`)}
selectsStart selectsStart={watch("end_date") ? true : false}
/> />
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>
@ -385,8 +414,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<Popover className="flex h-full items-center justify-center rounded-lg"> <Popover className="flex h-full items-center justify-center rounded-lg">
<> <>
<Popover.Button <Popover.Button
disabled={isCompleted ?? false} className={`text-sm font-medium text-custom-text-300 ${
className="cursor-default text-sm font-medium text-custom-text-300" isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
> >
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
</Popover.Button> </Popover.Button>
@ -409,10 +440,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
handleEndDateChange(val); handleEndDateChange(val);
} }
}} }}
startDate={watch("start_date") ? `${watch("start_date")}` : null} startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ? `${watch("end_date")}` : null} endDate={watch("end_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)} minDate={new Date(`${watch("start_date")}`)}
selectsEnd selectsEnd={watch("start_date") ? true : false}
/> />
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>
@ -528,6 +559,9 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}} }}
totalIssues={cycleDetails.total_issues} totalIssues={cycleDetails.total_issues}
isPeekView={Boolean(peekCycle)} isPeekView={Boolean(peekCycle)}
isCompleted={isCompleted}
filters={issueFilters?.filters}
handleFiltersUpdate={handleFiltersUpdate}
/> />
</div> </div>
)} )}

View File

@ -15,7 +15,6 @@ import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { IIssue } from "types"; import { IIssue } from "types";
type Props = { type Props = {
title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null; blocks: IGanttBlock[] | null;
enableReorder: boolean; enableReorder: boolean;
@ -33,7 +32,6 @@ type Props = {
export const IssueGanttSidebar: React.FC<Props> = (props) => { export const IssueGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { const {
title,
blockUpdateHandler, blockUpdateHandler,
blocks, blocks,
enableReorder, enableReorder,

View File

@ -155,7 +155,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
key={cycle.id} key={cycle.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${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.MenuItem>
))} ))}
</CustomMenu> </CustomMenu>
@ -192,20 +195,23 @@ export const CycleIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties} handleDisplayPropertiesUpdate={handleDisplayProperties}
/> />
</FiltersDropdown> </FiltersDropdown>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
{canUserCreateIssue && ( {canUserCreateIssue && (
<Button <>
onClick={() => { <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
setTrackElement("CYCLE_PAGE_HEADER"); Analytics
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE); </Button>
}} <Button
size="sm" onClick={() => {
prependIcon={<Plus />} setTrackElement("CYCLE_PAGE_HEADER");
> commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
Add Issue }}
</Button> size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)} )}
<button <button
type="button" type="button"

View File

@ -16,6 +16,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EFilterType } from "store/issues/types"; import { EFilterType } from "store/issues/types";
import { EUserWorkspaceRoles } from "constants/workspace";
const GLOBAL_VIEW_LAYOUTS = [ const GLOBAL_VIEW_LAYOUTS = [
{ key: "list", title: "List", link: "/workspace-views", icon: List }, { key: "list", title: "List", link: "/workspace-views", icon: List },
@ -38,7 +39,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
workspace: { workspaceLabels }, workspace: { workspaceLabels },
workspaceMember: { workspaceMembers }, workspaceMember: { workspaceMembers },
project: { workspaceProjects }, project: { workspaceProjects },
user: { currentWorkspaceRole },
workspaceGlobalIssuesFilter: { issueFilters, updateFilters }, workspaceGlobalIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
@ -77,6 +78,8 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
[workspaceSlug, updateFilters] [workspaceSlug, updateFilters]
); );
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<> <>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} /> <CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
@ -142,10 +145,11 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
</FiltersDropdown> </FiltersDropdown>
</> </>
)} )}
{isAuthorizedUser && (
<Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}> <Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}>
New View New View
</Button> </Button>
)}
</div> </div>
</div> </div>
</> </>

View File

@ -11,7 +11,7 @@ import { ProjectAnalyticsModal } from "components/analytics";
// ui // ui
import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui"; import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui";
// icons // icons
import { ArrowRight, ContrastIcon, Plus } from "lucide-react"; import { ArrowRight, Plus } from "lucide-react";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
@ -143,7 +143,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<CustomMenu <CustomMenu
label={ label={
<> <>
<ContrastIcon className="h-3 w-3" /> <DiceIcon className="h-3 w-3" />
{moduleDetails?.name && truncateText(moduleDetails.name, 40)} {moduleDetails?.name && truncateText(moduleDetails.name, 40)}
</> </>
} }
@ -156,7 +156,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
key={module.id} key={module.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/modules/${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.MenuItem>
))} ))}
</CustomMenu> </CustomMenu>
@ -193,20 +196,23 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties} handleDisplayPropertiesUpdate={handleDisplayProperties}
/> />
</FiltersDropdown> </FiltersDropdown>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
{canUserCreateIssue && ( {canUserCreateIssue && (
<Button <>
onClick={() => { <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
setTrackElement("MODULE_PAGE_HEADER"); Analytics
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE); </Button>
}} <Button
size="sm" onClick={() => {
prependIcon={<Plus />} setTrackElement("MODULE_PAGE_HEADER");
> commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
Add Issue }}
</Button> size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)} )}
<button <button
type="button" type="button"

View File

@ -202,20 +202,23 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</span> </span>
</Link> </Link>
)} )}
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
{canUserCreateIssue && ( {canUserCreateIssue && (
<Button <>
onClick={() => { <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
setTrackElement("PROJECT_PAGE_HEADER"); Analytics
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT); </Button>
}} <Button
size="sm" onClick={() => {
prependIcon={<Plus />} setTrackElement("PROJECT_PAGE_HEADER");
> commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
Add Issue }}
</Button> size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)} )}
</div> </div>
</div> </div>

View File

@ -138,7 +138,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
key={view.id} key={view.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/views/${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.MenuItem>
))} ))}
</CustomMenu> </CustomMenu>
@ -152,7 +155,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)} onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout} selectedLayout={activeLayout}
/> />
<FiltersDropdown title="Filters" placement="bottom-end">
<FiltersDropdown title="Filters" placement="bottom-end" disabled={!canUserCreateIssue}>
<FilterSelection <FilterSelection
filters={issueFilters?.filters ?? {}} filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
@ -175,7 +179,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties} handleDisplayPropertiesUpdate={handleDisplayProperties}
/> />
</FiltersDropdown> </FiltersDropdown>
{ {canUserCreateIssue && (
<Button <Button
onClick={() => { onClick={() => {
setTrackElement("PROJECT_VIEW_PAGE_HEADER"); setTrackElement("PROJECT_VIEW_PAGE_HEADER");
@ -186,7 +190,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
> >
Add Issue Add Issue
</Button> </Button>
} )}
</div> </div>
</div> </div>
); );

View File

@ -7,15 +7,24 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui"; import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectViewsHeader: React.FC = observer(() => { export const ProjectViewsHeader: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { project: projectStore, commandPalette } = useMobxStore(); const {
project: projectStore,
commandPalette,
user: { currentProjectRole },
} = useMobxStore();
const { currentProjectDetails } = projectStore; const { currentProjectDetails } = projectStore;
const canUserCreateIssue =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
return ( 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="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> </Breadcrumbs>
</div> </div>
</div> </div>
<div className="flex flex-shrink-0 items-center gap-2"> {canUserCreateIssue && (
<div> <div className="flex flex-shrink-0 items-center gap-2">
<Button <div>
variant="primary" <Button
size="sm" variant="primary"
prependIcon={<Plus className="h-3.5 w-3.5 stroke-2" />} size="sm"
onClick={() => commandPalette.toggleCreateViewModal(true)} prependIcon={<Plus className="h-3.5 w-3.5 stroke-2" />}
> onClick={() => commandPalette.toggleCreateViewModal(true)}
Create View >
</Button> Create View
</Button>
</div>
</div> </div>
</div> )}
</div> </div>
</> </>
); );

View File

@ -5,6 +5,8 @@ import { Breadcrumbs, Button } from "@plane/ui";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectsHeader = observer(() => { export const ProjectsHeader = observer(() => {
const router = useRouter(); const router = useRouter();
@ -15,10 +17,13 @@ export const ProjectsHeader = observer(() => {
project: projectStore, project: projectStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
trackEvent: { setTrackElement }, trackEvent: { setTrackElement },
user: { currentWorkspaceRole },
} = useMobxStore(); } = useMobxStore();
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : []; const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : [];
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return ( 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="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"> <div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
@ -44,17 +49,18 @@ export const ProjectsHeader = observer(() => {
/> />
</div> </div>
)} )}
{isAuthorizedUser && (
<Button <Button
prependIcon={<Plus />} prependIcon={<Plus />}
size="sm" size="sm"
onClick={() => { onClick={() => {
setTrackElement("PROJECTS_PAGE_HEADER"); setTrackElement("PROJECTS_PAGE_HEADER");
commandPaletteStore.toggleCreateProjectModal(true); commandPaletteStore.toggleCreateProjectModal(true);
}} }}
> >
Add Project Add Project
</Button> </Button>
)}
</div> </div>
</div> </div>
); );

View File

@ -165,16 +165,16 @@ export const InboxMainContent: React.FC = observer(() => {
issueStatus === -2 issueStatus === -2
? "border-yellow-500 bg-yellow-500/10 text-yellow-500" ? "border-yellow-500 bg-yellow-500/10 text-yellow-500"
: issueStatus === -1 : 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" ? "border-red-500 bg-red-500/10 text-red-500"
: issueStatus === 0 : "border-gray-500 bg-gray-500/10 text-custom-text-200"
? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() : issueStatus === 1
? "border-red-500 bg-red-500/10 text-red-500" ? "border-green-500 bg-green-500/10 text-green-500"
: "border-gray-500 bg-gray-500/10 text-custom-text-200" : issueStatus === 2
: issueStatus === 1 ? "border-gray-500 bg-gray-500/10 text-custom-text-200"
? "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 ? ( {issueStatus === -2 ? (
@ -225,7 +225,7 @@ export const InboxMainContent: React.FC = observer(() => {
</> </>
) : null} ) : null}
</div> </div>
<div className="mb-5 flex items-center"> <div className="mb-2.5 flex items-center">
{currentIssueState && ( {currentIssueState && (
<StateGroupIcon <StateGroupIcon
className="mr-3 h-4 w-4" className="mr-3 h-4 w-4"

View File

@ -21,6 +21,7 @@ export interface EmailFormValues {
EMAIL_HOST_PASSWORD: string; EMAIL_HOST_PASSWORD: string;
EMAIL_USE_TLS: string; EMAIL_USE_TLS: string;
// EMAIL_USE_SSL: string; // EMAIL_USE_SSL: string;
EMAIL_FROM: string;
} }
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => { export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
@ -45,6 +46,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
EMAIL_HOST_PASSWORD: config["EMAIL_HOST_PASSWORD"], EMAIL_HOST_PASSWORD: config["EMAIL_HOST_PASSWORD"],
EMAIL_USE_TLS: config["EMAIL_USE_TLS"], EMAIL_USE_TLS: config["EMAIL_USE_TLS"],
// EMAIL_USE_SSL: config["EMAIL_USE_SSL"], // EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
EMAIL_FROM: config["EMAIL_FROM"],
}, },
}); });
@ -168,6 +170,31 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
</div> </div>
</div> </div>
</div> </div>
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-x-16 gap-y-8 lg:grid-cols-2">
<div className="flex flex-col gap-1">
<h4 className="text-sm">From address</h4>
<Controller
control={control}
name="EMAIL_FROM"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_FROM"
name="EMAIL_FROM"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_FROM)}
placeholder="no-reply@projectplane.so"
className="w-full rounded-md font-medium"
/>
)}
/>
<p className="text-xs text-custom-text-400">
You will have to verify your email address to being sending emails.
</p>
</div>
</div>
<div className="flex w-full max-w-md flex-col gap-y-8 px-1"> <div className="flex w-full max-w-md flex-col gap-y-8 px-1">
<div className="mr-8 flex items-center gap-10 pt-4"> <div className="mr-8 flex items-center gap-10 pt-4">

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import React, { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -24,7 +24,14 @@ import { IIssueAttachment } from "types";
const issueAttachmentService = new IssueAttachmentService(); const issueAttachmentService = new IssueAttachmentService();
const projectMemberService = new ProjectMemberService(); const projectMemberService = new ProjectMemberService();
export const IssueAttachments = () => { type Props = {
editable: boolean;
};
export const IssueAttachments: React.FC<Props> = (props) => {
const { editable } = props;
// states
const [deleteAttachment, setDeleteAttachment] = useState<IIssueAttachment | null>(null); const [deleteAttachment, setDeleteAttachment] = useState<IIssueAttachment | null>(null);
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false); const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
@ -86,14 +93,16 @@ export const IssueAttachments = () => {
</div> </div>
</Link> </Link>
<button {editable && (
onClick={() => { <button
setDeleteAttachment(file); onClick={() => {
setAttachmentDeleteModal(true); setDeleteAttachment(file);
}} setAttachmentDeleteModal(true);
> }}
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" /> >
</button> <X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
</button>
)}
</div> </div>
))} ))}
</> </>

View File

@ -135,7 +135,9 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
debouncedFormSave(); debouncedFormSave();
}} }}
required 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 ${
!isAllowed ? "hover:cursor-not-allowed" : ""
}`}
hasError={Boolean(errors?.description)} hasError={Boolean(errors?.description)}
role="textbox" role="textbox"
disabled={!isAllowed} disabled={!isAllowed}
@ -170,7 +172,9 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
setShouldShowAlert={setShowAlert} setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting} setIsSubmitting={setIsSubmitting}
dragDropEnabled dragDropEnabled
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"} customClassName={
isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200 pointer-events-none"
}
noBorder={!isAllowed} noBorder={!isAllowed}
onChange={(description: Object, description_html: string) => { onChange={(description: Object, description_html: string) => {
setShowAlert(true); setShowAlert(true);

View File

@ -227,6 +227,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
reset({ reset({
...defaultValues, ...defaultValues,
...initialData, ...initialData,
project: projectId,
}); });
}, [setFocus, initialData, reset]); }, [setFocus, initialData, reset]);

View File

@ -120,8 +120,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => handleIssue={async (issueToUpdate, action: EIssueActions) =>
await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as IIssue, EIssueActions.UPDATE) await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as IIssue, action)
} }
/> />
)} )}

View File

@ -1,3 +1,4 @@
import { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Droppable } from "@hello-pangea/dnd"; import { Droppable } from "@hello-pangea/dnd";
// components // components
@ -48,11 +49,12 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
quickAddCallback, quickAddCallback,
viewId, viewId,
} = props; } = props;
const [showAllIssues, setShowAllIssues] = useState(false);
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
const issueIdList = groupedIssueIds ? groupedIssueIds[renderDateFormat(date.date)] : null; const issueIdList = groupedIssueIds ? groupedIssueIds[renderDateFormat(date.date)] : null;
const totalIssues = issueIdList?.length ?? 0;
return ( return (
<> <>
<div className="group relative flex h-full w-full flex-col bg-custom-background-90"> <div className="group relative flex h-full w-full flex-col bg-custom-background-90">
@ -87,7 +89,13 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{...provided.droppableProps} {...provided.droppableProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
<CalendarIssueBlocks issues={issues} issueIdList={issueIdList} quickActions={quickActions} /> <CalendarIssueBlocks
issues={issues}
issueIdList={issueIdList}
quickActions={quickActions}
showAllIssues={showAllIssues}
/>
{enableQuickIssueCreate && !disableIssueCreation && ( {enableQuickIssueCreate && !disableIssueCreation && (
<div className="px-2 py-1"> <div className="px-2 py-1">
<CalendarQuickAddIssueForm <CalendarQuickAddIssueForm
@ -98,9 +106,23 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
}} }}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
viewId={viewId} viewId={viewId}
onOpen={() => setShowAllIssues(true)}
/> />
</div> </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} {provided.placeholder}
</div> </div>
)} )}

View File

@ -10,30 +10,43 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { IIssueResponse } from "store/issues/types"; import { IIssueResponse } from "store/issues/types";
import { useMobxStore } from "lib/mobx/store-provider";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = { type Props = {
issues: IIssueResponse | undefined; issues: IIssueResponse | undefined;
issueIdList: string[] | null; issueIdList: string[] | null;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
showAllIssues?: boolean;
}; };
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => { export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const { issues, issueIdList, quickActions } = props; const { issues, issueIdList, quickActions, showAllIssues = false } = props;
// router // router
const router = useRouter(); const router = useRouter();
// states // states
const [isMenuActive, setIsMenuActive] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false);
// mobx store
const {
user: { currentProjectRole },
} = useMobxStore();
const menuActionRef = useRef<HTMLDivElement | null>(null); const menuActionRef = useRef<HTMLDivElement | null>(null);
const handleIssuePeekOverview = (issue: IIssue) => { const handleIssuePeekOverview = (issue: IIssue, event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
}
}; };
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
@ -50,21 +63,23 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
</div> </div>
); );
const isEditable = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<> <>
{issueIdList?.map((issueId, index) => { {issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => {
if (!issues?.[issueId]) return null; if (!issues?.[issueId]) return null;
const issue = issues?.[issueId]; const issue = issues?.[issueId];
return ( return (
<Draggable key={issue.id} draggableId={issue.id} index={index}> <Draggable key={issue.id} draggableId={issue.id} index={index} isDragDisabled={!isEditable}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className="relative cursor-pointer p-1 px-2" className="relative cursor-pointer p-1 px-2"
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
ref={provided.innerRef} ref={provided.innerRef}
onClick={() => handleIssuePeekOverview(issue)} onClick={(e) => handleIssuePeekOverview(issue, e)}
> >
{issue?.tempId !== undefined && ( {issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" /> <div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />

View File

@ -27,6 +27,7 @@ type Props = {
viewId?: string viewId?: string
) => Promise<IIssue | undefined>; ) => Promise<IIssue | undefined>;
viewId?: string; viewId?: string;
onOpen?: () => void;
}; };
const defaultValues: Partial<IIssue> = { const defaultValues: Partial<IIssue> = {
@ -57,7 +58,7 @@ const Inputs = (props: any) => {
}; };
export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => { export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props; const { formKey, groupId, prePopulatedData, quickAddCallback, viewId, onOpen } = props;
// router // router
const router = useRouter(); const router = useRouter();
@ -146,6 +147,11 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
} }
}; };
const handleOpen = () => {
setIsOpen(true);
if (onOpen) onOpen();
};
return ( return (
<> <>
{isOpen && ( {isOpen && (
@ -169,7 +175,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
<button <button
type="button" type="button"
className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100" 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" /> <PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span> <span className="text-sm font-medium text-custom-primary-100">New Issue</span>

View File

@ -14,6 +14,7 @@ export const CycleCalendarLayout: React.FC = observer(() => {
cycleIssues: cycleIssueStore, cycleIssues: cycleIssueStore,
cycleIssuesFilter: cycleIssueFilterStore, cycleIssuesFilter: cycleIssueFilterStore,
calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, calendarHelpers: { handleDragDrop: handleCalenderDragDrop },
cycle: { fetchCycleWithId },
} = useMobxStore(); } = useMobxStore();
const router = useRouter(); const router = useRouter();
@ -24,10 +25,12 @@ export const CycleCalendarLayout: React.FC = observer(() => {
if (!workspaceSlug || !cycleId) return; if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString()); await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString());
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
}, },
[EIssueActions.DELETE]: async (issue: IIssue) => { [EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId) return; if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString()); await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString());
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
}, },
[EIssueActions.REMOVE]: async (issue: IIssue) => { [EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId || !projectId || !issue.bridge_id) return; if (!workspaceSlug || !cycleId || !projectId || !issue.bridge_id) return;
@ -38,6 +41,7 @@ export const CycleCalendarLayout: React.FC = observer(() => {
issue.id, issue.id,
issue.bridge_id issue.bridge_id
); );
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
}, },
}; };

View File

@ -14,6 +14,7 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
moduleIssues: moduleIssueStore, moduleIssues: moduleIssueStore,
moduleIssuesFilter: moduleIssueFilterStore, moduleIssuesFilter: moduleIssueFilterStore,
calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, calendarHelpers: { handleDragDrop: handleCalenderDragDrop },
module: { fetchModuleDetails },
} = useMobxStore(); } = useMobxStore();
const router = useRouter(); const router = useRouter();
@ -27,14 +28,17 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
[EIssueActions.UPDATE]: async (issue: IIssue) => { [EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return; if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId); await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId);
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
}, },
[EIssueActions.DELETE]: async (issue: IIssue) => { [EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return; if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId); await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId);
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
}, },
[EIssueActions.REMOVE]: async (issue: IIssue) => { [EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId || !issue.bridge_id) return; if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id); await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id);
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
}, },
}; };

View File

@ -15,6 +15,8 @@ import emptyIssue from "public/empty-state/issue.svg";
// types // types
import { ISearchIssueResponse } from "types"; import { ISearchIssueResponse } from "types";
import { EProjectStore } from "store/command-palette.store"; import { EProjectStore } from "store/command-palette.store";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = { type Props = {
workspaceSlug: string | undefined; workspaceSlug: string | undefined;
@ -31,6 +33,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
cycleIssues: cycleIssueStore, cycleIssues: cycleIssueStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
trackEvent: { setTrackElement }, trackEvent: { setTrackElement },
user: { currentProjectRole: userRole },
} = useMobxStore(); } = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -49,6 +52,8 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
}); });
}; };
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<> <>
<ExistingIssuesListModal <ExistingIssuesListModal
@ -75,10 +80,12 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
variant="neutral-primary" variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />} prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setCycleIssuesListModal(true)} onClick={() => setCycleIssuesListModal(true)}
disabled={!isEditingAllowed}
> >
Add an existing issue Add an existing issue
</Button> </Button>
} }
disabled={!isEditingAllowed}
/> />
</div> </div>
</> </>

View File

@ -10,6 +10,8 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { ISearchIssueResponse } from "types"; import { ISearchIssueResponse } from "types";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useState } from "react"; import { useState } from "react";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = { type Props = {
workspaceSlug: string | undefined; workspaceSlug: string | undefined;
@ -26,6 +28,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
moduleIssues: moduleIssueStore, moduleIssues: moduleIssueStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
trackEvent: { setTrackElement }, trackEvent: { setTrackElement },
user: { currentProjectRole: userRole },
} = useMobxStore(); } = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -44,6 +47,8 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
); );
}; };
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<> <>
<ExistingIssuesListModal <ExistingIssuesListModal
@ -70,10 +75,12 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
variant="neutral-primary" variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />} prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setModuleIssuesListModal(true)} onClick={() => setModuleIssuesListModal(true)}
disabled={!isEditingAllowed}
> >
Add an existing issue Add an existing issue
</Button> </Button>
} }
disabled={!isEditingAllowed}
/> />
</div> </div>
</> </>

View File

@ -4,6 +4,8 @@ import { PlusIcon } from "lucide-react";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { NewEmptyState } from "components/common/new-empty-state"; import { NewEmptyState } from "components/common/new-empty-state";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// assets // assets
import emptyIssue from "public/empty-state/empty_issues.webp"; import emptyIssue from "public/empty-state/empty_issues.webp";
import { EProjectStore } from "store/command-palette.store"; import { EProjectStore } from "store/command-palette.store";
@ -12,8 +14,11 @@ export const ProjectEmptyState: React.FC = observer(() => {
const { const {
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
trackEvent: { setTrackElement }, trackEvent: { setTrackElement },
user: { currentProjectRole },
} = useMobxStore(); } = useMobxStore();
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<div className="grid h-full w-full place-items-center"> <div className="grid h-full w-full place-items-center">
<NewEmptyState <NewEmptyState
@ -26,14 +31,19 @@ export const ProjectEmptyState: React.FC = observer(() => {
description: description:
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
}} }}
primaryButton={{ primaryButton={
text: "Create your first issue", isEditingAllowed
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />, ? {
onClick: () => { text: "Create your first issue",
setTrackElement("PROJECT_EMPTY_STATE"); icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT); onClick: () => {
}, setTrackElement("PROJECT_EMPTY_STATE");
}} commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
},
}
: null
}
disabled={!isEditingAllowed}
/> />
</div> </div>
); );

View File

@ -1,5 +1,5 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { import {
AppliedDateFilters, AppliedDateFilters,
@ -16,6 +16,8 @@ import { X } from "lucide-react";
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types // types
import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types"; import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = { type Props = {
appliedFilters: IIssueFilterOptions; appliedFilters: IIssueFilterOptions;
@ -33,10 +35,16 @@ const dateFilters = ["start_date", "target_date"];
export const AppliedFiltersList: React.FC<Props> = observer((props) => { export const AppliedFiltersList: React.FC<Props> = observer((props) => {
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, projects, states } = props; const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, projects, states } = props;
const {
user: { currentProjectRole },
} = useMobxStore();
if (!appliedFilters) return null; if (!appliedFilters) return null;
if (Object.keys(appliedFilters).length === 0) return null; if (Object.keys(appliedFilters).length === 0) return null;
const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100"> <div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
{Object.entries(appliedFilters).map(([key, value]) => { {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"> <div className="flex flex-wrap items-center gap-1">
{membersFilters.includes(filterKey) && ( {membersFilters.includes(filterKey) && (
<AppliedMembersFilters <AppliedMembersFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter(filterKey, val)} handleRemove={(val) => handleRemoveFilter(filterKey, val)}
members={members} members={members}
values={value} values={value}
@ -63,16 +72,22 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
)} )}
{filterKey === "labels" && ( {filterKey === "labels" && (
<AppliedLabelsFilters <AppliedLabelsFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("labels", val)} handleRemove={(val) => handleRemoveFilter("labels", val)}
labels={labels} labels={labels}
values={value} values={value}
/> />
)} )}
{filterKey === "priority" && ( {filterKey === "priority" && (
<AppliedPriorityFilters handleRemove={(val) => handleRemoveFilter("priority", val)} values={value} /> <AppliedPriorityFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("priority", val)}
values={value}
/>
)} )}
{filterKey === "state" && states && ( {filterKey === "state" && states && (
<AppliedStateFilters <AppliedStateFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("state", val)} handleRemove={(val) => handleRemoveFilter("state", val)}
states={states} states={states}
values={value} values={value}
@ -86,30 +101,35 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
)} )}
{filterKey === "project" && ( {filterKey === "project" && (
<AppliedProjectFilters <AppliedProjectFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("project", val)} handleRemove={(val) => handleRemoveFilter("project", val)}
projects={projects} projects={projects}
values={value} values={value}
/> />
)} )}
<button {isEditingAllowed && (
type="button" <button
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" type="button"
onClick={() => handleRemoveFilter(filterKey, null)} className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
> onClick={() => handleRemoveFilter(filterKey, null)}
<X size={12} strokeWidth={2} /> >
</button> <X size={12} strokeWidth={2} />
</button>
)}
</div> </div>
</div> </div>
); );
})} })}
<button {isEditingAllowed && (
type="button" <button
onClick={handleClearAllFilters} type="button"
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" 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} /> Clear all
</button> <X size={12} strokeWidth={2} />
</button>
)}
</div> </div>
); );
}); });

View File

@ -9,10 +9,11 @@ type Props = {
handleRemove: (val: string) => void; handleRemove: (val: string) => void;
labels: IIssueLabel[] | undefined; labels: IIssueLabel[] | undefined;
values: string[]; values: string[];
editable: boolean | undefined;
}; };
export const AppliedLabelsFilters: React.FC<Props> = observer((props) => { export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
const { handleRemove, labels, values } = props; const { handleRemove, labels, values, editable } = props;
return ( return (
<> <>
@ -30,13 +31,15 @@ export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
}} }}
/> />
<span className="normal-case">{labelDetails.name}</span> <span className="normal-case">{labelDetails.name}</span>
<button {editable && (
type="button" <button
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" type="button"
onClick={() => handleRemove(labelId)} className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
> onClick={() => handleRemove(labelId)}
<X size={10} strokeWidth={2} /> >
</button> <X size={10} strokeWidth={2} />
</button>
)}
</div> </div>
); );
})} })}

View File

@ -9,10 +9,11 @@ type Props = {
handleRemove: (val: string) => void; handleRemove: (val: string) => void;
members: IUserLite[] | undefined; members: IUserLite[] | undefined;
values: string[]; values: string[];
editable: boolean | undefined;
}; };
export const AppliedMembersFilters: React.FC<Props> = observer((props) => { export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
const { handleRemove, members, values } = props; const { handleRemove, members, values, editable } = props;
return ( 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"> <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} /> <Avatar name={memberDetails.display_name} src={memberDetails.avatar} showTooltip={false} />
<span className="normal-case">{memberDetails.display_name}</span> <span className="normal-case">{memberDetails.display_name}</span>
<button {editable && (
type="button" <button
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" type="button"
onClick={() => handleRemove(memberId)} className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
> onClick={() => handleRemove(memberId)}
<X size={10} strokeWidth={2} /> >
</button> <X size={10} strokeWidth={2} />
</button>
)}
</div> </div>
); );
})} })}

View File

@ -9,10 +9,11 @@ import { TIssuePriorities } from "types";
type Props = { type Props = {
handleRemove: (val: string) => void; handleRemove: (val: string) => void;
values: string[]; values: string[];
editable: boolean | undefined;
}; };
export const AppliedPriorityFilters: React.FC<Props> = observer((props) => { export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values } = props; const { handleRemove, values, editable } = props;
return ( 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"> <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`} /> <PriorityIcon priority={priority as TIssuePriorities} className={`h-3 w-3`} />
{priority} {priority}
<button {editable && (
type="button" <button
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" type="button"
onClick={() => handleRemove(priority)} className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
> onClick={() => handleRemove(priority)}
<X size={10} strokeWidth={2} /> >
</button> <X size={10} strokeWidth={2} />
</button>
)}
</div> </div>
))} ))}
</> </>

View File

@ -10,10 +10,11 @@ type Props = {
handleRemove: (val: string) => void; handleRemove: (val: string) => void;
projects: IProject[] | undefined; projects: IProject[] | undefined;
values: string[]; values: string[];
editable: boolean | undefined;
}; };
export const AppliedProjectFilters: React.FC<Props> = observer((props) => { export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
const { handleRemove, projects, values } = props; const { handleRemove, projects, values, editable } = props;
return ( return (
<> <>
@ -34,13 +35,15 @@ export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
</span> </span>
)} )}
<span className="normal-case">{projectDetails.name}</span> <span className="normal-case">{projectDetails.name}</span>
<button {editable && (
type="button" <button
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" type="button"
onClick={() => handleRemove(projectId)} className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
> onClick={() => handleRemove(projectId)}
<X size={10} strokeWidth={2} /> >
</button> <X size={10} strokeWidth={2} />
</button>
)}
</div> </div>
); );
})} })}

View File

@ -10,10 +10,11 @@ type Props = {
handleRemove: (val: string) => void; handleRemove: (val: string) => void;
states: IState[]; states: IState[];
values: string[]; values: string[];
editable: boolean | undefined;
}; };
export const AppliedStateFilters: React.FC<Props> = observer((props) => { export const AppliedStateFilters: React.FC<Props> = observer((props) => {
const { handleRemove, states, values } = props; const { handleRemove, states, values, editable } = props;
return ( 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"> <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" /> <StateGroupIcon color={stateDetails.color} stateGroup={stateDetails.group} height="12px" width="12px" />
{stateDetails.name} {stateDetails.name}
<button {editable && (
type="button" <button
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200" type="button"
onClick={() => handleRemove(stateId)} className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
> onClick={() => handleRemove(stateId)}
<X size={10} strokeWidth={2} /> >
</button> <X size={10} strokeWidth={2} />
</button>
)}
</div> </div>
); );
})} })}

View File

@ -11,10 +11,11 @@ type Props = {
children: React.ReactNode; children: React.ReactNode;
title?: string; title?: string;
placement?: Placement; placement?: Placement;
disabled?: boolean;
}; };
export const FiltersDropdown: React.FC<Props> = (props) => { 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 [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | 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}> <Popover.Button as={React.Fragment}>
<Button <Button
disabled={disabled}
ref={setReferenceElement} ref={setReferenceElement}
variant="neutral-primary" variant="neutral-primary"
size="sm" size="sm"

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
@ -25,6 +25,8 @@ import {
IViewIssuesStore, IViewIssuesStore,
} from "store/issues"; } from "store/issues";
import { TUnGroupedIssues } from "store/issues/types"; import { TUnGroupedIssues } from "store/issues/types";
import { EIssueActions } from "../types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
interface IBaseGanttRoot { interface IBaseGanttRoot {
@ -35,10 +37,15 @@ interface IBaseGanttRoot {
| IViewIssuesFilterStore; | IViewIssuesFilterStore;
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore; issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
viewId?: string; viewId?: string;
issueActions: {
[EIssueActions.DELETE]: (issue: IIssue) => Promise<void>;
[EIssueActions.UPDATE]?: (issue: IIssue) => Promise<void>;
[EIssueActions.REMOVE]?: (issue: IIssue) => Promise<void>;
};
} }
export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => { export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => {
const { issueFiltersStore, issueStore, viewId } = props; const { issueFiltersStore, issueStore, viewId, issueActions } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query; const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
@ -64,11 +71,14 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, payload, viewId); await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, payload, viewId);
}; };
const updateIssue = async (projectId: string, issueId: string, payload: Partial<IIssue>) => { const handleIssues = useCallback(
if (!workspaceSlug) return; async (issue: IIssue, action: EIssueActions) => {
if (issueActions[action]) {
await issueStore.updateIssue(workspaceSlug.toString(), projectId, issueId, payload, viewId); await issueActions[action]!(issue);
}; }
},
[issueActions]
);
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
@ -102,8 +112,8 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => { handleIssue={async (issueToUpdate, action) => {
await updateIssue(peekProjectId.toString(), peekIssueId.toString(), issueToUpdate); await handleIssues(issueToUpdate as IIssue, action);
}} }}
/> />
)} )}

View File

@ -9,13 +9,17 @@ import { IIssue } from "types";
export const IssueGanttBlock = ({ data }: { data: IIssue }) => { export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
const router = useRouter(); const router = useRouter();
const handleIssuePeekOverview = () => { const handleIssuePeekOverview = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${data?.workspace_detail.slug}/projects/${data?.project_detail.id}/issues/${data?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
});
}
}; };
return ( return (

View File

@ -4,15 +4,50 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { BaseGanttRoot } from "./base-gantt-root"; import { BaseGanttRoot } from "./base-gantt-root";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// types
import { EIssueActions } from "../types";
import { IIssue } from "types";
export const CycleGanttLayout: React.FC = observer(() => { export const CycleGanttLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { cycleId } = router.query; const { cycleId, workspaceSlug } = router.query;
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore(); const {
cycleIssues: cycleIssueStore,
cycleIssuesFilter: cycleIssueFilterStore,
cycle: { fetchCycleWithId },
} = useMobxStore();
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString());
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString());
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
},
[EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
await cycleIssueStore.removeIssueFromCycle(
workspaceSlug.toString(),
issue.project,
cycleId.toString(),
issue.id,
issue.bridge_id
);
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
},
};
return ( return (
<BaseGanttRoot <BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={cycleIssueFilterStore} issueFiltersStore={cycleIssueFilterStore}
issueStore={cycleIssueStore} issueStore={cycleIssueStore}
viewId={cycleId?.toString()} viewId={cycleId?.toString()}

View File

@ -4,15 +4,50 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { BaseGanttRoot } from "./base-gantt-root"; import { BaseGanttRoot } from "./base-gantt-root";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// types
import { EIssueActions } from "../types";
import { IIssue } from "types";
export const ModuleGanttLayout: React.FC = observer(() => { export const ModuleGanttLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { moduleId } = router.query; const { moduleId, workspaceSlug } = router.query;
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore(); const {
moduleIssues: moduleIssueStore,
moduleIssuesFilter: moduleIssueFilterStore,
module: { fetchModuleDetails },
} = useMobxStore();
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
},
[EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
await moduleIssueStore.removeIssueFromModule(
workspaceSlug.toString(),
issue.project,
moduleId.toString(),
issue.id,
issue.bridge_id
);
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
},
};
return ( return (
<BaseGanttRoot <BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={moduleIssueFilterStore} issueFiltersStore={moduleIssueFilterStore}
issueStore={moduleIssueStore} issueStore={moduleIssueStore}
viewId={moduleId?.toString()} viewId={moduleId?.toString()}

View File

@ -1,12 +1,36 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { BaseGanttRoot } from "./base-gantt-root"; import { BaseGanttRoot } from "./base-gantt-root";
// types
import { EIssueActions } from "../types";
import { IIssue } from "types";
export const GanttLayout: React.FC = observer(() => { export const GanttLayout: React.FC = observer(() => {
const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore(); const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
return <BaseGanttRoot issueFiltersStore={projectIssueFiltersStore} issueStore={projectIssuesStore} />; const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await projectIssuesStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await projectIssuesStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
},
};
return (
<BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={projectIssueFiltersStore}
issueStore={projectIssuesStore}
/>
);
}); });

View File

@ -1,11 +1,35 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { BaseGanttRoot } from "./base-gantt-root"; import { BaseGanttRoot } from "./base-gantt-root";
// types
import { EIssueActions } from "../types";
import { IIssue } from "types";
export const ProjectViewGanttLayout: React.FC = observer(() => { export const ProjectViewGanttLayout: React.FC = observer(() => {
const { viewIssues: projectIssueViewStore, viewIssuesFilter: projectIssueViewFiltersStore } = useMobxStore(); const { viewIssues: projectIssueViewStore, viewIssuesFilter: projectIssueViewFiltersStore } = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
return <BaseGanttRoot issueFiltersStore={projectIssueViewFiltersStore} issueStore={projectIssueViewStore} />; const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await projectIssueViewStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await projectIssueViewStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
},
};
return (
<BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={projectIssueViewFiltersStore}
issueStore={projectIssueViewStore}
/>
);
}); });

View File

@ -346,8 +346,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => handleIssue={async (issueToUpdate, action: EIssueActions) =>
await handleIssues(sub_group_by, group_by, issueToUpdate as IIssue, EIssueActions.UPDATE) await handleIssues(sub_group_by, group_by, issueToUpdate as IIssue, action)
} }
/> />
)} )}

View File

@ -1,5 +1,5 @@
import { memo } from "react"; import { memo } from "react";
import { Draggable } from "@hello-pangea/dnd"; import { Draggable, DraggableStateSnapshot } from "@hello-pangea/dnd";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
// components // components
import { KanBanProperties } from "./properties"; import { KanBanProperties } from "./properties";
@ -32,11 +32,23 @@ interface IssueDetailsBlockProps {
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null; displayProperties: IIssueDisplayProperties | null;
isReadOnly: boolean; isReadOnly: boolean;
snapshot: DraggableStateSnapshot;
isDragDisabled: boolean;
} }
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => { const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
const { sub_group_id, columnId, issue, showEmptyGroup, handleIssues, quickActions, displayProperties, isReadOnly } = const {
props; sub_group_id,
columnId,
issue,
showEmptyGroup,
handleIssues,
quickActions,
displayProperties,
isReadOnly,
snapshot,
isDragDisabled,
} = props;
const router = useRouter(); const router = useRouter();
@ -44,20 +56,29 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE); if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
}; };
const handleIssuePeekOverview = () => { const handleIssuePeekOverview = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
}
}; };
return ( return (
<> <div
className={`flex flex-col space-y-2 cursor-pointer rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all w-full ${
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
onClick={handleIssuePeekOverview}
>
{displayProperties && displayProperties?.key && ( {displayProperties && displayProperties?.key && (
<div className="relative"> <div className="relative w-full ">
<div className="line-clamp-1 text-xs text-custom-text-300"> <div className="line-clamp-1 text-xs text-left text-custom-text-300">
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</div> </div>
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block"> <div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">
@ -70,9 +91,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
</div> </div>
)} )}
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="line-clamp-2 text-sm font-medium text-custom-text-100" onClick={handleIssuePeekOverview}> <div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
{issue.name}
</div>
</Tooltip> </Tooltip>
<div> <div>
<KanBanProperties <KanBanProperties
@ -85,7 +104,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
/> />
</div> </div>
</> </div>
); );
}; };
@ -121,10 +140,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
return ( return (
<> <>
<Draggable draggableId={draggableId} index={index}> <Draggable draggableId={draggableId} index={index} isDragDisabled={!canEditIssueProperties}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className="group/kanban-block relative p-1.5 hover:cursor-default" className="group/kanban-block relative p-1.5"
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
ref={provided.innerRef} ref={provided.innerRef}
@ -132,22 +151,18 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
{issue.tempId !== undefined && ( {issue.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" /> <div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)} )}
<div <KanbanIssueMemoBlock
className={`space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all ${ sub_group_id={sub_group_id}
isDragDisabled ? "" : "hover:cursor-grab" columnId={columnId}
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`} issue={issue}
> showEmptyGroup={showEmptyGroup}
<KanbanIssueMemoBlock handleIssues={handleIssues}
sub_group_id={sub_group_id} quickActions={quickActions}
columnId={columnId} displayProperties={displayProperties}
issue={issue} isReadOnly={!canEditIssueProperties}
showEmptyGroup={showEmptyGroup} snapshot={snapshot}
handleIssues={handleIssues} isDragDisabled={isDragDisabled}
quickActions={quickActions} />
displayProperties={displayProperties}
isReadOnly={!canEditIssueProperties}
/>
</div>
</div> </div>
)} )}
</Draggable> </Draggable>

View File

@ -57,7 +57,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
); );
}; };
const handleStartDate = (date: string) => { const handleStartDate = (date: string | null) => {
handleIssues( handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id, !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id, !group_id && group_id === "null" ? null : group_id,
@ -65,7 +65,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
); );
}; };
const handleTargetDate = (date: string) => { const handleTargetDate = (date: string | null) => {
handleIssues( handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id, !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id, !group_id && group_id === "null" ? null : group_id,
@ -122,7 +122,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
{displayProperties && displayProperties?.start_date && ( {displayProperties && displayProperties?.start_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.start_date || null} value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)} onChange={(date) => handleStartDate(date)}
disabled={isReadOnly} disabled={isReadOnly}
type="start_date" type="start_date"
/> />
@ -132,7 +132,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
{displayProperties && displayProperties?.due_date && ( {displayProperties && displayProperties?.due_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.target_date || null} value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)} onChange={(date) => handleTargetDate(date)}
disabled={isReadOnly} disabled={isReadOnly}
type="target_date" type="target_date"
/> />

View File

@ -25,6 +25,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
cycleIssuesFilter: cycleIssueFilterStore, cycleIssuesFilter: cycleIssueFilterStore,
cycleIssueKanBanView: cycleIssueKanBanViewStore, cycleIssueKanBanView: cycleIssueKanBanViewStore,
kanBanHelpers: kanBanHelperStore, kanBanHelpers: kanBanHelperStore,
cycle: { fetchCycleWithId },
} = useMobxStore(); } = useMobxStore();
const issueActions = { const issueActions = {
@ -32,11 +33,13 @@ export const CycleKanBanLayout: React.FC = observer(() => {
if (!workspaceSlug || !cycleId) return; if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString()); await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString());
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
}, },
[EIssueActions.DELETE]: async (issue: IIssue) => { [EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId) return; if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString()); await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString());
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
}, },
[EIssueActions.REMOVE]: async (issue: IIssue) => { [EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId || !issue.bridge_id) return; if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
@ -48,6 +51,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
issue.id, issue.id,
issue.bridge_id issue.bridge_id
); );
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
}, },
}; };

View File

@ -25,6 +25,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
moduleIssuesFilter: moduleIssueFilterStore, moduleIssuesFilter: moduleIssueFilterStore,
moduleIssueKanBanView: moduleIssueKanBanViewStore, moduleIssueKanBanView: moduleIssueKanBanViewStore,
kanBanHelpers: kanBanHelperStore, kanBanHelpers: kanBanHelperStore,
module: { fetchModuleDetails },
} = useMobxStore(); } = useMobxStore();
const issueActions = { const issueActions = {
@ -32,11 +33,13 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
if (!workspaceSlug || !moduleId) return; if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString()); await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
}, },
[EIssueActions.DELETE]: async (issue: IIssue) => { [EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return; if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, moduleId.toString()); await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
}, },
[EIssueActions.REMOVE]: async (issue: IIssue) => { [EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId || !issue.bridge_id) return; if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
@ -48,6 +51,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
issue.id, issue.id,
issue.bridge_id issue.bridge_id
); );
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
}, },
}; };

View File

@ -168,7 +168,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => await handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE)} handleIssue={async (issueToUpdate, action: EIssueActions) =>
await handleIssues(issueToUpdate as IIssue, action)
}
/> />
)} )}
</> </>

View File

@ -25,20 +25,27 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
handleIssues(issueToUpdate, EIssueActions.UPDATE); handleIssues(issueToUpdate, EIssueActions.UPDATE);
}; };
const handleIssuePeekOverview = () => { const handleIssuePeekOverview = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
}
}; };
const canEditIssueProperties = canEditProperties(issue.project); const canEditIssueProperties = canEditProperties(issue.project);
return ( return (
<> <>
<div className="relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm"> <button
className="relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm w-full"
onClick={handleIssuePeekOverview}
>
{displayProperties && displayProperties?.key && ( {displayProperties && displayProperties?.key && (
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300"> <div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
{issue?.project_detail?.identifier}-{issue.sequence_id} {issue?.project_detail?.identifier}-{issue.sequence_id}
@ -49,10 +56,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" /> <div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)} )}
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div <div className="line-clamp-1 w-full cursor-pointer text-sm font-medium text-custom-text-100 text-left">
className="line-clamp-1 w-full cursor-pointer text-sm font-medium text-custom-text-100"
onClick={handleIssuePeekOverview}
>
{issue.name} {issue.name}
</div> </div>
</Tooltip> </Tooltip>
@ -75,7 +79,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
</div> </div>
)} )}
</div> </div>
</div> </button>
</> </>
); );
}; };

View File

@ -40,11 +40,11 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids }); handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids });
}; };
const handleStartDate = (date: string) => { const handleStartDate = (date: string | null) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date }); handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date });
}; };
const handleTargetDate = (date: string) => { const handleTargetDate = (date: string | null) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date }); handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date });
}; };
@ -106,7 +106,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
{displayProperties && displayProperties?.start_date && ( {displayProperties && displayProperties?.start_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.start_date || null} value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)} onChange={(date) => handleStartDate(date)}
disabled={isReadonly} disabled={isReadonly}
type="start_date" type="start_date"
/> />
@ -116,7 +116,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
{displayProperties && displayProperties?.due_date && ( {displayProperties && displayProperties?.due_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.target_date || null} value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)} onChange={(date) => handleTargetDate(date)}
disabled={isReadonly} disabled={isReadonly}
type="target_date" type="target_date"
/> />

View File

@ -19,23 +19,30 @@ export const CycleListLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string }; const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
// store // store
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore(); const {
cycleIssues: cycleIssueStore,
cycleIssuesFilter: cycleIssueFilterStore,
cycle: { fetchCycleWithId },
} = useMobxStore();
const issueActions = { const issueActions = {
[EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => {
if (!workspaceSlug || !cycleId) return; if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId); await cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId);
fetchCycleWithId(workspaceSlug, issue.project, cycleId);
}, },
[EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => {
if (!workspaceSlug || !cycleId) return; if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId); await cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId);
fetchCycleWithId(workspaceSlug, issue.project, cycleId);
}, },
[EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => { [EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => {
if (!workspaceSlug || !cycleId || !issue.bridge_id) return; if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
await cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id); await cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id);
fetchCycleWithId(workspaceSlug, issue.project, cycleId);
}, },
}; };
const getProjects = (projectStore: IProjectStore) => { const getProjects = (projectStore: IProjectStore) => {

View File

@ -19,23 +19,30 @@ export const ModuleListLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string }; const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string };
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore(); const {
moduleIssues: moduleIssueStore,
moduleIssuesFilter: moduleIssueFilterStore,
module: { fetchModuleDetails },
} = useMobxStore();
const issueActions = { const issueActions = {
[EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => {
if (!workspaceSlug || !moduleId) return; if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId); await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId);
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
}, },
[EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => {
if (!workspaceSlug || !moduleId) return; if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId); await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId);
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
}, },
[EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => { [EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => {
if (!workspaceSlug || !moduleId || !issue.bridge_id) return; if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id); await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id);
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
}, },
}; };

View File

@ -42,7 +42,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
// store // store
const { const {
workspace: workspaceStore, workspace: workspaceStore,
projectMember: { projectMembers: _projectMembers, fetchProjectMembers }, projectMember: { members: _members, fetchProjectMembers },
} = useMobxStore(); } = useMobxStore();
const workspaceSlug = workspaceStore?.workspaceSlug; const workspaceSlug = workspaceStore?.workspaceSlug;
// states // states
@ -51,14 +51,14 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [isLoading, setIsLoading] = useState<Boolean>(false); const [isLoading, setIsLoading] = useState<Boolean>(false);
const getWorkspaceMembers = () => { const getProjectMembers = () => {
setIsLoading(true); setIsLoading(true);
if (workspaceSlug && projectId) fetchProjectMembers(workspaceSlug, projectId).then(() => setIsLoading(false)); if (workspaceSlug && projectId) fetchProjectMembers(workspaceSlug, projectId).then(() => setIsLoading(false));
}; };
const updatedDefaultOptions: IProjectMember[] = const updatedDefaultOptions: IProjectMember[] =
defaultOptions.map((member: any) => ({ member: { ...member } })) ?? []; defaultOptions.map((member: any) => ({ member: { ...member } })) ?? [];
const projectMembers = _projectMembers ?? updatedDefaultOptions; const projectMembers = projectId && _members[projectId] ? _members[projectId] : updatedDefaultOptions;
const options = projectMembers?.map((member) => ({ const options = projectMembers?.map((member) => ({
value: member.member.id, value: member.member.id,
@ -100,7 +100,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
const label = ( const label = (
<Tooltip tooltipHeading="Assignee" tooltipContent={getTooltipContent()} position="top"> <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) ? ( {value && value.length > 0 && Array.isArray(value) ? (
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>
{value.map((assigneeId) => { {value.map((assigneeId) => {
@ -142,7 +142,10 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
className={`flex w-full items-center justify-between gap-1 text-xs ${ className={`flex w-full items-center justify-between gap-1 text-xs ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer" disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => !projectMembers && getWorkspaceMembers()} onClick={(e) => {
e.stopPropagation();
(!projectId || !_members[projectId]) && getProjectMembers();
}}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
@ -168,7 +171,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
<div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}> <div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}>
{isLoading ? ( {isLoading ? (
<p className="text-center text-custom-text-200">Loading...</p> <p className="text-center text-custom-text-200">Loading...</p>
) : filteredOptions.length > 0 ? ( ) : filteredOptions && filteredOptions.length > 0 ? (
filteredOptions.map((option) => ( filteredOptions.map((option) => (
<Combobox.Option <Combobox.Option
key={option.value} key={option.value}
@ -178,6 +181,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
active && !selected ? "bg-custom-background-80" : "" active && !selected ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={(e) => e.stopPropagation()}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -12,11 +12,11 @@ import { Tooltip } from "@plane/ui";
// hooks // hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
// helpers // helpers
import { renderDateFormat } from "helpers/date-time.helper"; import { renderDateFormat, renderFormattedDate } from "helpers/date-time.helper";
export interface IIssuePropertyDate { export interface IIssuePropertyDate {
value: any; value: string | null;
onChange: (date: any) => void; onChange: (date: string | null) => void;
disabled?: boolean; disabled?: boolean;
type: "start_date" | "target_date"; type: "start_date" | "target_date";
} }
@ -56,32 +56,41 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
return ( return (
<> <>
<Popover.Button <Popover.Button
as="button"
type="button"
ref={dropdownBtn} ref={dropdownBtn}
className={`flex h-5 w-full items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 outline-none duration-300 ${ className="border-none outline-none"
disabled onClick={(e) => e.stopPropagation()}
? "pointer-events-none cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
}`}
> >
<div className="flex items-center justify-center gap-2 overflow-hidden"> <Tooltip
<dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} /> tooltipHeading={dateOptionDetails.placeholder}
{value && ( tooltipContent={value ? renderFormattedDate(value) : "None"}
<> >
<Tooltip tooltipHeading={dateOptionDetails.placeholder} tooltipContent={value ?? "None"}> <div
<div className="text-xs">{value}</div> className={`flex h-5 w-full items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 outline-none duration-300 ${
</Tooltip> disabled
? "pointer-events-none cursor-not-allowed text-custom-text-200"
<div : "cursor-pointer hover:bg-custom-background-80"
className="flex flex-shrink-0 items-center justify-center" }`}
onClick={() => { >
if (onChange) onChange(null); <div className="flex items-center justify-center gap-2 overflow-hidden">
}} <dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} />
> {value && (
<X className="h-2.5 w-2.5" strokeWidth={2} /> <>
</div> <div className="text-xs">{value}</div>
</> <div
)} className="flex flex-shrink-0 items-center justify-center"
</div> onClick={() => {
if (onChange) onChange(null);
}}
>
<X className="h-2.5 w-2.5" strokeWidth={2} />
</div>
</>
)}
</div>
</div>
</Tooltip>
</Popover.Button> </Popover.Button>
<div className={`${open ? "fixed left-0 top-0 z-20 h-full w-full cursor-auto" : ""}`}> <div className={`${open ? "fixed left-0 top-0 z-20 h-full w-full cursor-auto" : ""}`}>
@ -92,7 +101,8 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
{({ close }) => ( {({ close }) => (
<DatePicker <DatePicker
selected={value ? new Date(value) : new Date()} selected={value ? new Date(value) : new Date()}
onChange={(val: any) => { onChange={(val, e) => {
e?.stopPropagation();
if (onChange && val) { if (onChange && val) {
onChange(renderDateFormat(val)); onChange(renderDateFormat(val));
close(); close();

View File

@ -116,6 +116,7 @@ export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observe
className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs ${ className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-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 hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={(e) => e.stopPropagation()}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
@ -150,6 +151,7 @@ export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observe
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={(e) => e.stopPropagation()}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -107,7 +107,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
{projectLabels {projectLabels
?.filter((l) => value.includes(l.id)) ?.filter((l) => value.includes(l.id))
.map((label) => ( .map((label) => (
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={label.name ?? ""}> <Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name ?? ""}>
<div <div
key={label.id} key={label.id}
className={`flex overflow-hidden hover:bg-custom-background-80 ${ className={`flex overflow-hidden hover:bg-custom-background-80 ${
@ -145,14 +145,16 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
</div> </div>
) )
) : ( ) : (
<div <Tooltip position="top" tooltipHeading="Labels" tooltipContent="None">
className={`h-full flex items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${ <div
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300" 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"
> }`}
<Tags className="h-3.5 w-3.5" strokeWidth={2} /> >
{placeholderText} <Tags className="h-3.5 w-3.5" strokeWidth={2} />
</div> {placeholderText}
</div>
</Tooltip>
)} )}
</div> </div>
); );
@ -177,7 +179,10 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
? "cursor-pointer" ? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => !storeLabels && fetchLabels()} onClick={(e) => {
e.stopPropagation();
!storeLabels && fetchLabels();
}}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
@ -214,6 +219,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
selected ? "text-custom-text-100" : "text-custom-text-200" selected ? "text-custom-text-100" : "text-custom-text-200"
}` }`
} }
onClick={(e) => e.stopPropagation()}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -121,7 +121,10 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs ${ className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-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 hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => !storeStates && fetchProjectStates()} onClick={(e) => {
e.stopPropagation();
!storeStates && fetchProjectStates();
}}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
@ -157,6 +160,7 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={(e) => e.stopPropagation()}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -58,7 +58,12 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
}} }}
currentStore={EProjectStore.PROJECT} currentStore={EProjectStore.PROJECT}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis> <CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -40,7 +40,12 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
handleClose={() => setDeleteIssueModal(false)} handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete} onSubmit={handleDelete}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis> <CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -58,7 +58,12 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
}} }}
currentStore={EProjectStore.CYCLE} currentStore={EProjectStore.CYCLE}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis> <CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -58,7 +58,13 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
}} }}
currentStore={EProjectStore.MODULE} currentStore={EProjectStore.MODULE}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
<CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -68,7 +68,12 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
}} }}
currentStore={EProjectStore.PROJECT} currentStore={EProjectStore.PROJECT}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis> <CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -9,7 +9,7 @@ import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot } from "compon
export const ArchivedIssueLayoutRoot: React.FC = observer(() => { export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; const { workspaceSlug, projectId } = router.query;
const { const {
projectArchivedIssues: { getIssues, fetchIssues }, projectArchivedIssues: { getIssues, fetchIssues },
@ -18,8 +18,8 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
useSWR(workspaceSlug && projectId ? `ARCHIVED_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => { useSWR(workspaceSlug && projectId ? `ARCHIVED_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
if (workspaceSlug && projectId) { if (workspaceSlug && projectId) {
await fetchFilters(workspaceSlug, projectId); await fetchFilters(workspaceSlug.toString(), projectId.toString());
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader"); await fetchIssues(workspaceSlug.toString(), projectId.toString(), getIssues ? "mutation" : "init-loader");
} }
}); });

View File

@ -24,11 +24,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
const [transferIssuesModal, setTransferIssuesModal] = useState(false); const [transferIssuesModal, setTransferIssuesModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query as { const { workspaceSlug, projectId, cycleId } = router.query;
workspaceSlug: string;
projectId: string;
cycleId: string;
};
const { const {
cycle: cycleStore, cycle: cycleStore,
@ -40,8 +36,13 @@ export const CycleLayoutRoot: React.FC = observer(() => {
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null, workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
async () => { async () => {
if (workspaceSlug && projectId && cycleId) { if (workspaceSlug && projectId && cycleId) {
await fetchFilters(workspaceSlug, projectId, cycleId); await fetchFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", cycleId); await fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
getIssues ? "mutation" : "init-loader",
cycleId.toString()
);
} }
} }
); );
@ -69,7 +70,11 @@ export const CycleLayoutRoot: React.FC = observer(() => {
) : ( ) : (
<> <>
{Object.keys(getIssues ?? {}).length == 0 ? ( {Object.keys(getIssues ?? {}).length == 0 ? (
<CycleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} /> <CycleEmptyState
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
cycleId={cycleId?.toString()}
/>
) : ( ) : (
<div className="h-full w-full overflow-auto"> <div className="h-full w-full overflow-auto">
{activeLayout === "list" ? ( {activeLayout === "list" ? (

View File

@ -11,7 +11,7 @@ import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
export const DraftIssueLayoutRoot: React.FC = observer(() => { export const DraftIssueLayoutRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; const { workspaceSlug, projectId } = router.query;
const { const {
projectDraftIssuesFilter: { issueFilters, fetchFilters }, projectDraftIssuesFilter: { issueFilters, fetchFilters },
@ -20,8 +20,8 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
useSWR(workspaceSlug && projectId ? `DRAFT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => { useSWR(workspaceSlug && projectId ? `DRAFT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
if (workspaceSlug && projectId) { if (workspaceSlug && projectId) {
await fetchFilters(workspaceSlug, projectId); await fetchFilters(workspaceSlug.toString(), projectId.toString());
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader"); await fetchIssues(workspaceSlug.toString(), projectId.toString(), getIssues ? "mutation" : "init-loader");
} }
}); });

View File

@ -20,11 +20,7 @@ import { Spinner } from "@plane/ui";
export const ModuleLayoutRoot: React.FC = observer(() => { export const ModuleLayoutRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query as { const { workspaceSlug, projectId, moduleId } = router.query;
workspaceSlug: string;
projectId: string;
moduleId: string;
};
const { const {
moduleIssues: { loader, getIssues, fetchIssues }, moduleIssues: { loader, getIssues, fetchIssues },
@ -35,8 +31,13 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
workspaceSlug && projectId && moduleId ? `MODULE_ISSUES_V3_${workspaceSlug}_${projectId}_${moduleId}` : null, workspaceSlug && projectId && moduleId ? `MODULE_ISSUES_V3_${workspaceSlug}_${projectId}_${moduleId}` : null,
async () => { async () => {
if (workspaceSlug && projectId && moduleId) { if (workspaceSlug && projectId && moduleId) {
await fetchFilters(workspaceSlug, projectId, moduleId); await fetchFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", moduleId); await fetchIssues(
workspaceSlug.toString(),
projectId.toString(),
getIssues ? "mutation" : "init-loader",
moduleId.toString()
);
} }
} }
); );
@ -54,7 +55,11 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
) : ( ) : (
<> <>
{Object.keys(getIssues ?? {}).length == 0 ? ( {Object.keys(getIssues ?? {}).length == 0 ? (
<ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} /> <ModuleEmptyState
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
moduleId={moduleId?.toString()}
/>
) : ( ) : (
<div className="h-full w-full overflow-auto"> <div className="h-full w-full overflow-auto">
{activeLayout === "list" ? ( {activeLayout === "list" ? (

View File

@ -19,7 +19,7 @@ import { Spinner } from "@plane/ui";
export const ProjectLayoutRoot: React.FC = observer(() => { export const ProjectLayoutRoot: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; const { workspaceSlug, projectId } = router.query;
const { const {
projectIssues: { loader, getIssues, fetchIssues }, projectIssues: { loader, getIssues, fetchIssues },
@ -28,8 +28,8 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => { useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => {
if (workspaceSlug && projectId) { if (workspaceSlug && projectId) {
await fetchFilters(workspaceSlug, projectId); await fetchFilters(workspaceSlug.toString(), projectId.toString());
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader"); await fetchIssues(workspaceSlug.toString(), projectId.toString(), getIssues ? "mutation" : "init-loader");
} }
}); });

View File

@ -18,11 +18,7 @@ import { Spinner } from "@plane/ui";
export const ProjectViewLayoutRoot: React.FC = observer(() => { export const ProjectViewLayoutRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query as { const { workspaceSlug, projectId, viewId } = router.query;
workspaceSlug: string;
projectId: string;
viewId?: string;
};
const { const {
viewIssues: { loader, getIssues, fetchIssues }, viewIssues: { loader, getIssues, fetchIssues },
@ -31,8 +27,8 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
useSWR(workspaceSlug && projectId && viewId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => { useSWR(workspaceSlug && projectId && viewId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => {
if (workspaceSlug && projectId && viewId) { if (workspaceSlug && projectId && viewId) {
await fetchFilters(workspaceSlug, projectId, viewId); await fetchFilters(workspaceSlug.toString(), projectId.toString(), viewId.toString());
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader"); await fetchIssues(workspaceSlug.toString(), projectId.toString(), getIssues ? "mutation" : "init-loader");
} }
}); });

View File

@ -10,7 +10,7 @@ import { IIssue, IUserLite } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
members: IUserLite[] | undefined; members: IUserLite[] | undefined;
onChange: (data: Partial<IIssue>) => void; onChange: (issue: IIssue, data: Partial<IIssue>) => void;
expandedIssues: string[]; expandedIssues: string[];
disabled: boolean; disabled: boolean;
}; };
@ -18,7 +18,7 @@ type Props = {
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onChange, expandedIssues, disabled }) => { export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1; 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 ( return (
<> <>
@ -26,8 +26,13 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
projectId={issue.project_detail?.id ?? null} projectId={issue.project_detail?.id ?? null}
value={issue.assignees} value={issue.assignees}
defaultOptions={issue?.assignee_details ? issue.assignee_details : []} defaultOptions={issue?.assignee_details ? issue.assignee_details : []}
onChange={(data) => onChange({ assignees: data })} onChange={(data) => {
className="h-11 w-full border-b-[0.5px] border-custom-border-200" 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 " buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
noLabelBorder noLabelBorder
hideDropdownArrow hideDropdownArrow

View File

@ -18,7 +18,7 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
return ( return (
<> <>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200"> <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"} {issue.attachment_count} {issue.attachment_count === 1 ? "attachment" : "attachments"}
</div> </div>

View File

@ -19,7 +19,7 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
return ( return (
<> <>
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200"> <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)} {renderLongDetailDateFormat(issue.created_at)}
</div> </div>

View File

@ -9,7 +9,7 @@ import { IIssue } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
onChange: (data: Partial<IIssue>) => void; onChange: (issue: IIssue, data: Partial<IIssue>) => void;
expandedIssues: string[]; expandedIssues: string[];
disabled: boolean; disabled: boolean;
}; };
@ -17,14 +17,19 @@ type Props = {
export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => { export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1; 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 ( return (
<> <>
<ViewDueDateSelect <ViewDueDateSelect
issue={issue} issue={issue}
onChange={(val) => onChange({ target_date: val })} onChange={(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" 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 noBorder
disabled={disabled} disabled={disabled}
/> />

View File

@ -7,7 +7,7 @@ import { IIssue } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
onChange: (formData: Partial<IIssue>) => void; onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
expandedIssues: string[]; expandedIssues: string[];
disabled: boolean; disabled: boolean;
}; };
@ -17,15 +17,20 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1; 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 ( return (
<> <>
<IssuePropertyEstimates <IssuePropertyEstimates
projectId={issue.project_detail?.id ?? null} projectId={issue.project_detail?.id ?? null}
value={issue.estimate_point} value={issue.estimate_point}
onChange={(data) => onChange({ estimate_point: data })} onChange={(data) => {
className="h-11 w-full border-b-[0.5px] border-custom-border-200" 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" buttonClassName="h-full w-full px-2.5 py-1 !shadow-none !border-0"
hideDropdownArrow hideDropdownArrow
disabled={disabled} disabled={disabled}

View File

@ -34,13 +34,17 @@ export const IssueColumn: React.FC<Props> = ({
const menuActionRef = useRef<HTMLDivElement | null>(null); const menuActionRef = useRef<HTMLDivElement | null>(null);
const handleIssuePeekOverview = (issue: IIssue) => { const handleIssuePeekOverview = (issue: IIssue, event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
}
}; };
const paddingLeft = `${nestingLevel * 54}px`; const paddingLeft = `${nestingLevel * 54}px`;
@ -99,7 +103,7 @@ export const IssueColumn: React.FC<Props> = ({
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div <div
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100" className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
onClick={() => handleIssuePeekOverview(issue)} onClick={(e) => handleIssuePeekOverview(issue, e)}
> >
{issue.name} {issue.name}
</div> </div>

View File

@ -9,7 +9,7 @@ import { IIssue, IIssueLabel } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
onChange: (formData: Partial<IIssue>) => void; onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
labels: IIssueLabel[] | undefined; labels: IIssueLabel[] | undefined;
expandedIssues: string[]; expandedIssues: string[];
disabled: boolean; disabled: boolean;
@ -20,7 +20,7 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1; 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 ( return (
<> <>
@ -28,8 +28,13 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
projectId={issue.project_detail?.id ?? null} projectId={issue.project_detail?.id ?? null}
value={issue.labels} value={issue.labels}
defaultOptions={issue?.label_details ? issue.label_details : []} defaultOptions={issue?.label_details ? issue.label_details : []}
onChange={(data) => onChange({ labels: data })} onChange={(data) => {
className="h-11 w-full border-b-[0.5px] border-custom-border-200" 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" buttonClassName="px-2.5 h-full"
hideDropdownArrow hideDropdownArrow
maxRender={1} maxRender={1}

View File

@ -18,7 +18,7 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
return ( return (
<> <>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200"> <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"} {issue.link_count} {issue.link_count === 1 ? "link" : "links"}
</div> </div>

View File

@ -9,7 +9,7 @@ import { IIssue } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
onChange: (data: Partial<IIssue>) => void; onChange: (issue: IIssue, data: Partial<IIssue>) => void;
expandedIssues: string[]; expandedIssues: string[];
disabled: boolean; disabled: boolean;
}; };
@ -17,14 +17,19 @@ type Props = {
export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => { export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1; 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 ( return (
<> <>
<PrioritySelect <PrioritySelect
value={issue.priority} value={issue.priority}
onChange={(data) => onChange({ priority: data })} onChange={(data) => {
className="h-11 w-full border-b-[0.5px] border-custom-border-200" 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" buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1"
showTitle showTitle
highlightUrgentPriority={false} highlightUrgentPriority={false}

View File

@ -9,7 +9,7 @@ import { IIssue } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
onChange: (formData: Partial<IIssue>) => void; onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
expandedIssues: string[]; expandedIssues: string[];
disabled: boolean; disabled: boolean;
}; };
@ -17,14 +17,19 @@ type Props = {
export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => { export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1; 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 ( return (
<> <>
<ViewStartDateSelect <ViewStartDateSelect
issue={issue} issue={issue}
onChange={(val) => onChange({ start_date: val })} onChange={(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" 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 noBorder
disabled={disabled} disabled={disabled}
/> />

View File

@ -9,7 +9,7 @@ import { IIssue, IState } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
onChange: (data: Partial<IIssue>) => void; onChange: (issue: IIssue, data: Partial<IIssue>) => void;
states: IState[] | undefined; states: IState[] | undefined;
expandedIssues: string[]; expandedIssues: string[];
disabled: boolean; disabled: boolean;
@ -20,7 +20,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1; 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 ( return (
<> <>
@ -28,7 +28,12 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
projectId={issue.project_detail?.id ?? null} projectId={issue.project_detail?.id ?? null}
value={issue.state} value={issue.state}
defaultOptions={issue?.state_detail ? [issue.state_detail] : []} defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
onChange={(data) => onChange({ state: data.id, state_detail: data })} 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" className="w-full !h-11 border-b-[0.5px] border-custom-border-200"
buttonClassName="!shadow-none !border-0 h-full w-full" buttonClassName="!shadow-none !border-0 h-full w-full"
hideDropdownArrow hideDropdownArrow

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