Merge pull request #3171 from makeplane/develop

Promote: develop change to preview
This commit is contained in:
sriram veeraghanta 2023-12-18 15:37:53 +05:30 committed by GitHub
commit 6f2cce081f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
182 changed files with 1881 additions and 2469 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

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

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

@ -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,43 @@ 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 } = useMobxStore();
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString());
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, 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
);
},
};
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,43 @@ 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 } = useMobxStore();
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString());
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, 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
);
},
};
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

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

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

View File

@ -18,7 +18,7 @@ export const SpreadsheetSubIssueColumn: 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.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div> </div>

View File

@ -21,7 +21,7 @@ export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
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.updated_at)} {renderLongDetailDateFormat(issue.updated_at)}
</div> </div>

View File

@ -163,18 +163,13 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
{issues?.map((issue) => { {issues?.map((issue) => {
const disableUserActions = !canEditProperties(issue.project); const disableUserActions = !canEditProperties(issue.project);
return ( return (
<div <div key={`${property}-${issue.id}`} className={`h-fit ${disableUserActions ? "" : "cursor-pointer"}`}>
key={`${property}-${issue.id}`}
className={`h-fit ${
disableUserActions ? "" : "cursor-pointer hover:bg-custom-background-80"
}`}
>
{property === "state" ? ( {property === "state" ? (
<SpreadsheetStateColumn <SpreadsheetStateColumn
disabled={disableUserActions} disabled={disableUserActions}
expandedIssues={expandedIssues} expandedIssues={expandedIssues}
issue={issue} issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)} onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
states={states} states={states}
/> />
) : property === "priority" ? ( ) : property === "priority" ? (
@ -182,14 +177,14 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
disabled={disableUserActions} disabled={disableUserActions}
expandedIssues={expandedIssues} expandedIssues={expandedIssues}
issue={issue} issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)} onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/> />
) : property === "estimate" ? ( ) : property === "estimate" ? (
<SpreadsheetEstimateColumn <SpreadsheetEstimateColumn
disabled={disableUserActions} disabled={disableUserActions}
expandedIssues={expandedIssues} expandedIssues={expandedIssues}
issue={issue} issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)} onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/> />
) : property === "assignee" ? ( ) : property === "assignee" ? (
<SpreadsheetAssigneeColumn <SpreadsheetAssigneeColumn
@ -197,7 +192,7 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
expandedIssues={expandedIssues} expandedIssues={expandedIssues}
issue={issue} issue={issue}
members={members} members={members}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)} onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/> />
) : property === "labels" ? ( ) : property === "labels" ? (
<SpreadsheetLabelColumn <SpreadsheetLabelColumn
@ -205,21 +200,21 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
expandedIssues={expandedIssues} expandedIssues={expandedIssues}
issue={issue} issue={issue}
labels={labels} labels={labels}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)} onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/> />
) : property === "start_date" ? ( ) : property === "start_date" ? (
<SpreadsheetStartDateColumn <SpreadsheetStartDateColumn
disabled={disableUserActions} disabled={disableUserActions}
expandedIssues={expandedIssues} expandedIssues={expandedIssues}
issue={issue} issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)} onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/> />
) : property === "due_date" ? ( ) : property === "due_date" ? (
<SpreadsheetDueDateColumn <SpreadsheetDueDateColumn
disabled={disableUserActions} disabled={disableUserActions}
expandedIssues={expandedIssues} expandedIssues={expandedIssues}
issue={issue} issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)} onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/> />
) : property === "created_on" ? ( ) : property === "created_on" ? (
<SpreadsheetCreatedOnColumn expandedIssues={expandedIssues} issue={issue} /> <SpreadsheetCreatedOnColumn expandedIssues={expandedIssues} issue={issue} />

View File

@ -194,7 +194,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate: any) => await handleIssues(issueToUpdate, EIssueActions.UPDATE)} handleIssue={async (issueToUpdate: any, action: EIssueActions) => await handleIssues(issueToUpdate, action)}
/> />
)} )}
</div> </div>

View File

@ -41,7 +41,7 @@ const issueService = new IssueService();
const issueCommentService = new IssueCommentService(); const issueCommentService = new IssueCommentService();
export const IssueMainContent: React.FC<Props> = observer((props) => { export const IssueMainContent: React.FC<Props> = observer((props) => {
const { issueDetails, submitChanges, uneditable = false } = props; const { issueDetails, submitChanges, uneditable } = props;
// states // states
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
// router // router
@ -152,7 +152,9 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
); );
}; };
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const isAllowed =
(!!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER) ||
(uneditable !== undefined && !uneditable);
return ( return (
<> <>
@ -216,7 +218,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
</CustomMenu> </CustomMenu>
</div> </div>
) : null} ) : null}
<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"
@ -232,7 +234,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
workspaceSlug={workspaceSlug as string} workspaceSlug={workspaceSlug as string}
issue={issueDetails} issue={issueDetails}
handleFormSubmit={submitChanges} handleFormSubmit={submitChanges}
isAllowed={isAllowed || !uneditable} isAllowed={isAllowed}
/> />
{workspaceSlug && projectId && ( {workspaceSlug && projectId && (
@ -250,8 +252,8 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
<div className="flex flex-col gap-3 py-3"> <div className="flex flex-col gap-3 py-3">
<h3 className="text-lg">Attachments</h3> <h3 className="text-lg">Attachments</h3>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<IssueAttachmentUpload disabled={uneditable} /> <IssueAttachmentUpload disabled={!isAllowed} />
<IssueAttachments /> <IssueAttachments editable={isAllowed} />
</div> </div>
</div> </div>
<div className="space-y-5 pt-3"> <div className="space-y-5 pt-3">
@ -264,7 +266,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
/> />
<AddComment <AddComment
onSubmit={handleAddComment} onSubmit={handleAddComment}
disabled={uneditable} disabled={!isAllowed}
showAccessSpecifier={projectDetails && projectDetails.is_deployed} showAccessSpecifier={projectDetails && projectDetails.is_deployed}
/> />
</div> </div>

View File

@ -153,10 +153,12 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
debouncedFormSave(); debouncedFormSave();
}} }}
required={true} required={true}
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent !p-0 text-xl outline-none ring-0 focus:!px-3 focus:!py-2 focus:ring-1 focus:ring-custom-primary" className={`min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent !p-0 text-xl outline-none ring-0 focus:!px-3 focus:!py-2 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={!true} disabled={!isAllowed}
/> />
)} )}
/> />
@ -188,7 +190,9 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (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

@ -47,7 +47,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
} = useMobxStore(); } = useMobxStore();
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, peekProjectId: projectId } = router.query;
const handleState = (_state: string) => { const handleState = (_state: string) => {
issueUpdate({ ...issue, state: _state }); issueUpdate({ ...issue, state: _state });
@ -116,7 +116,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<p>State</p> <p>State</p>
</div> </div>
<div> <div>
<SidebarStateSelect value={issue?.state || ""} onChange={handleState} disabled={disableUserActions} /> <SidebarStateSelect
value={issue?.state || ""}
projectId={projectId as string}
onChange={handleState}
disabled={disableUserActions}
/>
</div> </div>
</div> </div>
@ -129,6 +134,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div> <div>
<SidebarAssigneeSelect <SidebarAssigneeSelect
value={issue.assignees || []} value={issue.assignees || []}
projectId={projectId as string}
onChange={handleAssignee} onChange={handleAssignee}
disabled={disableUserActions} disabled={disableUserActions}
/> />
@ -210,7 +216,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<p>Parent</p> <p>Parent</p>
</div> </div>
<div> <div>
<SidebarParentSelect onChange={handleParent} issueDetails={issue} disabled={disableUserActions} /> <SidebarParentSelect
onChange={handleParent}
issueDetails={issue}
projectId={projectId as string}
disabled={disableUserActions}
/>
</div> </div>
</div> </div>
</div> </div>
@ -226,6 +237,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div> <div>
<SidebarCycleSelect <SidebarCycleSelect
issueDetail={issue} issueDetail={issue}
projectId={projectId as string}
disabled={disableUserActions} disabled={disableUserActions}
handleIssueUpdate={handleCycleOrModuleChange} handleIssueUpdate={handleCycleOrModuleChange}
/> />
@ -240,6 +252,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div> <div>
<SidebarModuleSelect <SidebarModuleSelect
issueDetail={issue} issueDetail={issue}
projectId={projectId as string}
disabled={disableUserActions} disabled={disableUserActions}
handleIssueUpdate={handleCycleOrModuleChange} handleIssueUpdate={handleCycleOrModuleChange}
/> />
@ -253,6 +266,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div className="flex w-full flex-col gap-3"> <div className="flex w-full flex-col gap-3">
<SidebarLabelSelect <SidebarLabelSelect
issueDetails={issue} issueDetails={issue}
projectId={projectId as string}
labelList={issue.labels} labelList={issue.labels}
submitChanges={handleLabels} submitChanges={handleLabels}
isNotAllowed={disableUserActions} isNotAllowed={disableUserActions}

View File

@ -11,6 +11,7 @@ import { IssueView } from "components/issues";
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
// types // types
import { IIssue, IIssueLink } from "types"; import { IIssue, IIssueLink } from "types";
import { EIssueActions } from "../issue-layouts/types";
// constants // constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
@ -18,7 +19,7 @@ interface IIssuePeekOverview {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
issueId: string; issueId: string;
handleIssue: (issue: Partial<IIssue>) => void; handleIssue: (issue: Partial<IIssue>, action: EIssueActions) => Promise<void>;
isArchived?: boolean; isArchived?: boolean;
children?: ReactNode; children?: ReactNode;
} }
@ -30,8 +31,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { peekIssueId } = router.query; const { peekIssueId } = router.query;
const { const {
user: { currentProjectRole },
issue: { removeIssueFromStructure },
issueDetail: { issueDetail: {
createIssueComment, createIssueComment,
updateIssueComment, updateIssueComment,
@ -58,6 +57,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}, },
archivedIssues: { deleteArchivedIssue }, archivedIssues: { deleteArchivedIssue },
project: { currentProjectDetails }, project: { currentProjectDetails },
workspaceMember: { currentWorkspaceUserProjectsRole },
} = useMobxStore(); } = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -98,7 +98,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const issueUpdate = async (_data: Partial<IIssue>) => { const issueUpdate = async (_data: Partial<IIssue>) => {
if (handleIssue) { if (handleIssue) {
await handleIssue(_data); await handleIssue(_data, EIssueActions.UPDATE);
fetchIssueActivity(workspaceSlug, projectId, issueId); fetchIssueActivity(workspaceSlug, projectId, issueId);
} }
}; };
@ -133,7 +133,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const handleDeleteIssue = async () => { const handleDeleteIssue = async () => {
if (isArchived) await deleteArchivedIssue(workspaceSlug, projectId, issue!); if (isArchived) await deleteArchivedIssue(workspaceSlug, projectId, issue!);
else removeIssueFromStructure(workspaceSlug, projectId, issue!); else await handleIssue(issue!, EIssueActions.DELETE);
const { query } = router; const { query } = router;
if (query.peekIssueId) { if (query.peekIssueId) {
setPeekId(null); setPeekId(null);
@ -146,7 +146,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
} }
}; };
const userRole = currentProjectRole ?? EUserWorkspaceRoles.GUEST; const userRole =
(currentWorkspaceUserProjectsRole && currentWorkspaceUserProjectsRole[projectId]) ?? EUserWorkspaceRoles.GUEST;
return ( return (
<Fragment> <Fragment>

View File

@ -1,4 +1,4 @@
import { FC, ReactNode, useState } from "react"; import { FC, ReactNode, useRef, 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 useSWR from "swr"; import useSWR from "swr";
@ -14,6 +14,8 @@ import {
PeekOverviewIssueDetails, PeekOverviewIssueDetails,
PeekOverviewProperties, PeekOverviewProperties,
} from "components/issues"; } from "components/issues";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui // ui
import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui"; import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
// types // types
@ -107,6 +109,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek"); const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
// ref
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
const updateRoutePeekId = () => { const updateRoutePeekId = () => {
if (issueId != peekIssueId) { if (issueId != peekIssueId) {
@ -151,6 +155,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode); const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode);
useOutsideClickDetector(issuePeekOverviewRef, () => removeRoutePeekId());
return ( return (
<> <>
{issue && !isArchived && ( {issue && !isArchived && (
@ -178,6 +184,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
{issueId === peekIssueId && ( {issueId === peekIssueId && (
<div <div
ref={issuePeekOverviewRef}
className={`fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300 className={`fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300
${peekMode === "side-peek" ? `bottom-0 right-0 top-0 w-full md:w-[50%]` : ``} ${peekMode === "side-peek" ? `bottom-0 right-0 top-0 w-full md:w-[50%]` : ``}
${peekMode === "modal" ? `left-[50%] top-[50%] h-5/6 w-5/6 -translate-x-[50%] -translate-y-[50%]` : ``} ${peekMode === "modal" ? `left-[50%] top-[50%] h-5/6 w-5/6 -translate-x-[50%] -translate-y-[50%]` : ``}

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