Merge branch 'refactor/mobx-store' of github.com:makeplane/plane into refactor/mobx-store

This commit is contained in:
NarayanBavisetti 2023-12-15 16:24:33 +05:30
commit 9e9699d2d3
152 changed files with 2206 additions and 2900 deletions

View File

@ -33,8 +33,8 @@ The backend is a django project which is kept inside apiserver
1. Clone the repo 1. Clone the repo
```bash ```bash
git clone https://github.com/makeplane/plane git clone https://github.com/makeplane/plane.git [folder-name]
cd plane cd [folder-name]
chmod +x setup.sh chmod +x setup.sh
``` ```
@ -44,33 +44,12 @@ chmod +x setup.sh
./setup.sh ./setup.sh
``` ```
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file 3. Start the containers
```bash ```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env docker compose -f docker-compose-local.yml up
``` ```
```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
```
4. Run Docker compose up
```bash
docker compose up -d
```
5. Install dependencies
```bash
yarn install
```
6. Run the web app in development mode
```bash
yarn dev
```
## Missing a Feature? ## Missing a Feature?

View File

@ -37,7 +37,7 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases. > Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting). The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting/docker-compose).
## ⚡️ Contributors Quick Start ## ⚡️ Contributors Quick Start
@ -63,7 +63,7 @@ Thats it!
## 🍙 Self Hosting ## 🍙 Self Hosting
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
## 🚀 Features ## 🚀 Features

View File

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

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

@ -0,0 +1,31 @@
#!/bin/bash
set -e
python manage.py wait_for_db
python manage.py migrate
# Create the default bucket
#!/bin/bash
# Collect system information
HOSTNAME=$(hostname)
MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
CPU_INFO=$(cat /proc/cpuinfo)
MEMORY_INFO=$(free -h)
DISK_INFO=$(df -h)
# Concatenate information and compute SHA-256 hash
SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
# Export the variables
export MACHINE_SIGNATURE=$SIGNATURE
# Register instance
python manage.py register_instance $MACHINE_SIGNATURE
# Load the configuration variable
python manage.py configure_instance
# Create the default bucket
python manage.py create_bucket
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local

View File

@ -145,6 +145,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
) )
) )
) )
.prefetch_related(
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=self.kwargs.get("slug"),
is_active=True,
).select_related("member"),
to_attr="members_list",
)
)
.distinct() .distinct()
) )
@ -160,16 +170,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
projects = ( projects = (
self.get_queryset() self.get_queryset()
.annotate(sort_order=Subquery(sort_order_query)) .annotate(sort_order=Subquery(sort_order_query))
.prefetch_related(
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=slug,
is_active=True,
).select_related("member"),
to_attr="members_list",
)
)
.order_by("sort_order", "name") .order_by("sort_order", "name")
) )
if request.GET.get("per_page", False) and request.GET.get("cursor", False): if request.GET.get("per_page", False) and request.GET.get("cursor", False):
@ -676,6 +676,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,

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
@ -501,6 +502,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

@ -1,9 +1,9 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// cmdk
import { Command } from "cmdk"; import { Command } from "cmdk";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useProjectState } from "hooks/store";
// ui // ui
import { Spinner, StateGroupIcon } from "@plane/ui"; import { Spinner, StateGroupIcon } from "@plane/ui";
// icons // icons
@ -18,14 +18,14 @@ type Props = {
export const ChangeIssueState: React.FC<Props> = observer((props) => { export const ChangeIssueState: React.FC<Props> = observer((props) => {
const { closePalette, issue } = props; const { closePalette, issue } = props;
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks
const { const {
projectState: { projectStates },
projectIssues: { updateIssue }, projectIssues: { updateIssue },
} = useMobxStore(); } = useMobxStore();
const { projectStates } = useProjectState();
const submitChanges = async (formData: Partial<IIssue>) => { const submitChanges = async (formData: Partial<IIssue>) => {
if (!workspaceSlug || !projectId || !issue) return; if (!workspaceSlug || !projectId || !issue) return;

View File

@ -1,7 +1,8 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useLabel } from "hooks/store";
// hook // hook
import useEstimateOption from "hooks/use-estimate-option"; import useEstimateOption from "hooks/use-estimate-option";
// icons // icons
@ -27,7 +28,6 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper"; import { capitalizeFirstLetter } from "helpers/string.helper";
// types // types
import { IIssueActivity } from "types"; import { IIssueActivity } from "types";
import { useEffect } from "react";
const IssueLink = ({ activity }: { activity: IIssueActivity }) => { const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
const router = useRouter(); const router = useRouter();
@ -74,11 +74,10 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
}; };
const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => { const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => {
// store hooks
const { const {
workspace: { labels, fetchWorkspaceLabels }, workspaceLabel: { workspaceLabels, fetchWorkspaceLabels },
} = useMobxStore(); } = useLabel();
const workspaceLabels = labels[workspaceSlug];
useEffect(() => { useEffect(() => {
if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug); if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug);

View File

@ -4,8 +4,7 @@ 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";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useApplication, useCycle } from "hooks/store";
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { SingleProgressStats } from "components/core"; import { SingleProgressStats } from "components/core";
@ -71,20 +70,20 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = props; const { workspaceSlug, projectId } = props;
// store hooks // store hooks
const { cycle: cycleStore } = useMobxStore();
const { const {
commandPalette: { toggleCreateCycleModal }, commandPalette: { toggleCreateCycleModal },
} = useApplication(); } = useApplication();
const { fetchActiveCycle, projectActiveCycle, getActiveCycleById, addCycleToFavorites, removeCycleFromFavorites } =
useCycle();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
useSWR( const { isLoading } = useSWR(
workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null,
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
); );
const activeCycle = cycleStore.cycles?.[projectId]?.current || null; const activeCycle = projectActiveCycle ? getActiveCycleById(projectActiveCycle) : null;
const cycle = activeCycle ? activeCycle[0] : null;
const issues = (cycleStore?.active_cycle_issues as any) || null; const issues = (cycleStore?.active_cycle_issues as any) || null;
// const { data: issues } = useSWR( // const { data: issues } = useSWR(
@ -97,14 +96,14 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
// : null // : null
// ) as { data: IIssue[] | undefined }; // ) as { data: IIssue[] | undefined };
if (!cycle) if (!activeCycle && isLoading)
return ( return (
<Loader> <Loader>
<Loader.Item height="250px" /> <Loader.Item height="250px" />
</Loader> </Loader>
); );
if (!cycle) if (!activeCycle)
return ( return (
<div className="grid h-full place-items-center text-center"> <div className="grid h-full place-items-center text-center">
<div className="space-y-2"> <div className="space-y-2">
@ -129,24 +128,24 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</div> </div>
); );
const endDate = new Date(cycle.end_date ?? ""); const endDate = new Date(activeCycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? ""); const startDate = new Date(activeCycle.start_date ?? "");
const groupedIssues: any = { const groupedIssues: any = {
backlog: cycle.backlog_issues, backlog: activeCycle.backlog_issues,
unstarted: cycle.unstarted_issues, unstarted: activeCycle.unstarted_issues,
started: cycle.started_issues, started: activeCycle.started_issues,
completed: cycle.completed_issues, completed: activeCycle.completed_issues,
cancelled: cycle.cancelled_issues, cancelled: activeCycle.cancelled_issues,
}; };
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); const cycleStatus = getDateRangeStatus(activeCycle.start_date, activeCycle.end_date);
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => { const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -159,7 +158,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -171,7 +170,10 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
const progressIndicatorData = stateGroups.map((group, index) => ({ const progressIndicatorData = stateGroups.map((group, index) => ({
id: index, id: index,
name: group.title, name: group.title,
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0, value:
activeCycle.total_issues > 0
? ((activeCycle[group.key as keyof ICycle] as number) / activeCycle.total_issues) * 100
: 0,
color: group.color, color: group.color,
})); }));
@ -199,8 +201,8 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
}`} }`}
/> />
</span> </span>
<Tooltip tooltipContent={cycle.name} position="top-left"> <Tooltip tooltipContent={activeCycle.name} position="top-left">
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 70)}</h3> <h3 className="break-words text-lg font-semibold">{truncateText(activeCycle.name, 70)}</h3>
</Tooltip> </Tooltip>
</span> </span>
<span className="flex items-center gap-1 capitalize"> <span className="flex items-center gap-1 capitalize">
@ -221,19 +223,19 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
{cycleStatus === "current" ? ( {cycleStatus === "current" ? (
<span className="flex gap-1 whitespace-nowrap"> <span className="flex gap-1 whitespace-nowrap">
<RunningIcon className="h-4 w-4" /> <RunningIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left {findHowManyDaysLeft(activeCycle.end_date ?? new Date())} Days Left
</span> </span>
) : cycleStatus === "upcoming" ? ( ) : cycleStatus === "upcoming" ? (
<span className="flex gap-1 whitespace-nowrap"> <span className="flex gap-1 whitespace-nowrap">
<AlarmClock className="h-4 w-4" /> <AlarmClock className="h-4 w-4" />
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left {findHowManyDaysLeft(activeCycle.start_date ?? new Date())} Days Left
</span> </span>
) : cycleStatus === "completed" ? ( ) : cycleStatus === "completed" ? (
<span className="flex gap-1 whitespace-nowrap"> <span className="flex gap-1 whitespace-nowrap">
{cycle.total_issues - cycle.completed_issues > 0 && ( {activeCycle.total_issues - activeCycle.completed_issues > 0 && (
<Tooltip <Tooltip
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${ tooltipContent={`${activeCycle.total_issues - activeCycle.completed_issues} more pending ${
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues" activeCycle.total_issues - activeCycle.completed_issues === 1 ? "issue" : "issues"
}`} }`}
> >
<span> <span>
@ -247,7 +249,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cycleStatus cycleStatus
)} )}
</span> </span>
{cycle.is_favorite ? ( {activeCycle.is_favorite ? (
<button <button
onClick={(e) => { onClick={(e) => {
handleRemoveFromFavorites(e); handleRemoveFromFavorites(e);
@ -281,26 +283,26 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2.5 text-custom-text-200"> <div className="flex items-center gap-2.5 text-custom-text-200">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( {activeCycle.owned_by.avatar && activeCycle.owned_by.avatar !== "" ? (
<img <img
src={cycle.owned_by.avatar} src={activeCycle.owned_by.avatar}
height={16} height={16}
width={16} width={16}
className="rounded-full" className="rounded-full"
alt={cycle.owned_by.display_name} alt={activeCycle.owned_by.display_name}
/> />
) : ( ) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize"> <span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
{cycle.owned_by.display_name.charAt(0)} {activeCycle.owned_by.display_name.charAt(0)}
</span> </span>
)} )}
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span> <span className="text-custom-text-200">{activeCycle.owned_by.display_name}</span>
</div> </div>
{cycle.assignees.length > 0 && ( {activeCycle.assignees.length > 0 && (
<div className="flex items-center gap-1 text-custom-text-200"> <div className="flex items-center gap-1 text-custom-text-200">
<AvatarGroup> <AvatarGroup>
{cycle.assignees.map((assignee) => ( {activeCycle.assignees.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} /> <Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))} ))}
</AvatarGroup> </AvatarGroup>
@ -311,15 +313,15 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<div className="flex items-center gap-4 text-custom-text-200"> <div className="flex items-center gap-4 text-custom-text-200">
<div className="flex gap-2"> <div className="flex gap-2">
<LayersIcon className="h-4 w-4 flex-shrink-0" /> <LayersIcon className="h-4 w-4 flex-shrink-0" />
{cycle.total_issues} issues {activeCycle.total_issues} issues
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StateGroupIcon stateGroup="completed" height="14px" width="14px" /> <StateGroupIcon stateGroup="completed" height="14px" width="14px" />
{cycle.completed_issues} issues {activeCycle.completed_issues} issues
</div> </div>
</div> </div>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${activeCycle.id}`}>
<span className="w-full rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90"> <span className="w-full rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90">
View Cycle View Cycle
</span> </span>
@ -350,14 +352,14 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</div> </div>
} }
completed={groupedIssues[group]} completed={groupedIssues[group]}
total={cycle.total_issues} total={activeCycle.total_issues}
/> />
))} ))}
</div> </div>
</div> </div>
</div> </div>
<div className="h-60 overflow-y-scroll border-custom-border-200"> <div className="h-60 overflow-y-scroll border-custom-border-200">
<ActiveCycleProgressStats cycle={cycle} /> <ActiveCycleProgressStats cycle={activeCycle} />
</div> </div>
</div> </div>
</div> </div>
@ -469,15 +471,18 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<span> <span>
<LayersIcon className="h-5 w-5 flex-shrink-0 text-custom-text-200" /> <LayersIcon className="h-5 w-5 flex-shrink-0 text-custom-text-200" />
</span> </span>
<span>Pending Issues - {cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)}</span> <span>
Pending Issues -{" "}
{activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)}
</span>
</div> </div>
</div> </div>
<div className="relative h-64"> <div className="relative h-64">
<ProgressChart <ProgressChart
distribution={cycle.distribution?.completion_chart ?? {}} distribution={activeCycle.distribution?.completion_chart ?? {}}
startDate={cycle.start_date ?? ""} startDate={activeCycle.start_date ?? ""}
endDate={cycle.end_date ?? ""} endDate={activeCycle.end_date ?? ""}
totalIssues={cycle.total_issues} totalIssues={activeCycle.total_issues}
/> />
</div> </div>
</div> </div>

View File

@ -1,10 +1,8 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider"; // hooks
import { useCycle } from "hooks/store";
// components // components
import { CycleDetailsSidebar } from "./sidebar"; import { CycleDetailsSidebar } from "./sidebar";
@ -14,14 +12,13 @@ type Props = {
}; };
export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug }) => { export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug }) => {
// router
const router = useRouter(); const router = useRouter();
const { peekCycle } = router.query; const { peekCycle } = router.query;
// refs
const ref = React.useRef(null); const ref = React.useRef(null);
// store hooks
const { cycle: cycleStore } = useMobxStore(); const { fetchCycleDetails } = useCycle();
const { fetchCycleWithId } = cycleStore;
const handleClose = () => { const handleClose = () => {
delete router.query.peekCycle; delete router.query.peekCycle;
@ -33,8 +30,8 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
useEffect(() => { useEffect(() => {
if (!peekCycle) return; if (!peekCycle) return;
fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString()); fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
}, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]); }, [fetchCycleDetails, peekCycle, projectId, workspaceSlug]);
return ( return (
<> <>

View File

@ -2,6 +2,7 @@ import { FC, MouseEvent, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
// hooks // hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
@ -17,10 +18,6 @@ import {
renderShortMonthDate, renderShortMonthDate,
} from "helpers/date-time.helper"; } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types
import { ICycle } from "types";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// constants // constants
import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
@ -28,61 +25,33 @@ import { EUserWorkspaceRoles } from "constants/workspace";
export interface ICyclesBoardCard { export interface ICyclesBoardCard {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
cycle: ICycle; cycleId: string;
} }
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => { export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
const { cycle, workspaceSlug, projectId } = props; const { cycleId, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { setTrackElement },
user: userStore,
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
// states // states
const [updateModal, setUpdateModal] = useState(false); const [updateModal, setUpdateModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
// computed // router
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const isDateValid = cycle.start_date || cycle.end_date;
const { currentProjectRole } = userStore;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const router = useRouter(); const router = useRouter();
// store
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); const {
eventTracker: { setTrackElement },
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); } = useApplication();
const {
const cycleTotalIssues = membership: { currentProjectRole },
cycle.backlog_issues + } = useUser();
cycle.unstarted_issues + const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle();
cycle.started_issues + // toast alert
cycle.completed_issues + const { setToastAlert } = useToast();
cycle.cancelled_issues;
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
const issueCount = cycle
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycle.completed_issues
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => { const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Link Copied!", title: "Link Copied!",
@ -95,7 +64,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -108,7 +77,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -137,14 +106,48 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...query, peekCycle: cycle.id }, query: { ...query, peekCycle: cycleId },
}); });
}; };
const cycleDetails = getCycleById(cycleId);
if (!cycleDetails) return null;
// computed
const cycleStatus = getDateRangeStatus(cycleDetails.start_date, cycleDetails.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const isDateValid = cycleDetails.start_date || cycleDetails.end_date;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const cycleTotalIssues =
cycleDetails.backlog_issues +
cycleDetails.unstarted_issues +
cycleDetails.started_issues +
cycleDetails.completed_issues +
cycleDetails.cancelled_issues;
const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100;
const issueCount = cycleDetails
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycleDetails.completed_issues
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";
return ( return (
<div> <div>
<CycleCreateUpdateModal <CycleCreateUpdateModal
data={cycle} data={cycleDetails}
isOpen={updateModal} isOpen={updateModal}
handleClose={() => setUpdateModal(false)} handleClose={() => setUpdateModal(false)}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
@ -152,22 +155,22 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
/> />
<CycleDeleteModal <CycleDeleteModal
cycle={cycle} cycle={cycleDetails}
isOpen={deleteModal} isOpen={deleteModal}
handleClose={() => setDeleteModal(false)} handleClose={() => setDeleteModal(false)}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="flex h-44 w-full min-w-[250px] flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md"> <div className="flex h-44 w-full min-w-[250px] flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3 truncate"> <div className="flex items-center gap-3 truncate">
<span className="flex-shrink-0"> <span className="flex-shrink-0">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" /> <CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
</span> </span>
<Tooltip tooltipContent={cycle.name} position="top"> <Tooltip tooltipContent={cycleDetails.name} position="top">
<span className="truncate text-base font-medium">{cycle.name}</span> <span className="truncate text-base font-medium">{cycleDetails.name}</span>
</Tooltip> </Tooltip>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -180,7 +183,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
}} }}
> >
{currentCycle.value === "current" {currentCycle.value === "current"
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
: `${currentCycle.label}`} : `${currentCycle.label}`}
</span> </span>
)} )}
@ -196,11 +199,11 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
<LayersIcon className="h-4 w-4 text-custom-text-300" /> <LayersIcon className="h-4 w-4 text-custom-text-300" />
<span className="text-xs text-custom-text-300">{issueCount}</span> <span className="text-xs text-custom-text-300">{issueCount}</span>
</div> </div>
{cycle.assignees.length > 0 && ( {cycleDetails.assignees.length > 0 && (
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}> <Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
<div className="flex cursor-default items-center gap-1"> <div className="flex cursor-default items-center gap-1">
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>
{cycle.assignees.map((assignee) => ( {cycleDetails.assignees.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} /> <Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))} ))}
</AvatarGroup> </AvatarGroup>
@ -241,7 +244,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
)} )}
<div className="z-10 flex items-center gap-1.5"> <div className="z-10 flex items-center gap-1.5">
{isEditingAllowed && {isEditingAllowed &&
(cycle.is_favorite ? ( (cycleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}> <button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 fill-current text-amber-500" /> <Star className="h-3.5 w-3.5 fill-current text-amber-500" />
</button> </button>

View File

@ -4,11 +4,9 @@ import { observer } from "mobx-react-lite";
import { useApplication } from "hooks/store"; import { useApplication } from "hooks/store";
// components // components
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
// types
import { ICycle } from "types";
export interface ICyclesBoard { export interface ICyclesBoard {
cycles: ICycle[]; cycleIds: string[];
filter: string; filter: string;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
@ -16,13 +14,13 @@ export interface ICyclesBoard {
} }
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => { export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId, peekCycle } = props; const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props;
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
return ( return (
<> <>
{cycles.length > 0 ? ( {cycleIds?.length > 0 ? (
<div className="h-full w-full"> <div className="h-full w-full">
<div className="flex h-full w-full justify-between"> <div className="flex h-full w-full justify-between">
<div <div
@ -32,8 +30,8 @@ export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" : "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
} auto-rows-max transition-all `} } auto-rows-max transition-all `}
> >
{cycles.map((cycle) => ( {cycleIds.map((cycleId) => (
<CyclesBoardCard key={cycle.id} workspaceSlug={workspaceSlug} projectId={projectId} cycle={cycle} /> <CyclesBoardCard key={cycleId} workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} />
))} ))}
</div> </div>
<CyclePeekOverview <CyclePeekOverview

View File

@ -1,10 +1,8 @@
import { FC, MouseEvent, useState } from "react"; import { FC, MouseEvent, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// stores
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
@ -20,14 +18,12 @@ import {
renderShortMonthDate, renderShortMonthDate,
} from "helpers/date-time.helper"; } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types
import { ICycle } from "types";
// constants // constants
import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_STATUS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
type TCyclesListItem = { type TCyclesListItem = {
cycle: ICycle; cycleId: string;
handleEditCycle?: () => void; handleEditCycle?: () => void;
handleDeleteCycle?: () => void; handleDeleteCycle?: () => void;
handleAddToFavorites?: () => void; handleAddToFavorites?: () => void;
@ -37,52 +33,29 @@ type TCyclesListItem = {
}; };
export const CyclesListItem: FC<TCyclesListItem> = (props) => { export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const { cycle, workspaceSlug, projectId } = props; const { cycleId, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { setTrackElement },
user: userStore,
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
// states // states
const [updateModal, setUpdateModal] = useState(false); const [updateModal, setUpdateModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false);
// computed // router
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const { currentProjectRole } = userStore;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const router = useRouter(); const router = useRouter();
// store hooks
const cycleTotalIssues = const {
cycle.backlog_issues + eventTracker: { setTrackElement },
cycle.unstarted_issues + } = useApplication();
cycle.started_issues + const {
cycle.completed_issues + membership: { currentProjectRole },
cycle.cancelled_issues; } = useUser();
const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle();
const renderDate = cycle.start_date || cycle.end_date; // toast alert
const { setToastAlert } = useToast();
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => { const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Link Copied!", title: "Link Copied!",
@ -95,7 +68,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -108,7 +81,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault(); e.preventDefault();
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -137,27 +110,56 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...query, peekCycle: cycle.id }, query: { ...query, peekCycle: cycleId },
}); });
}; };
const cycleDetails = getCycleById(cycleId);
if (!cycleDetails) return null;
// computed
const cycleStatus = getDateRangeStatus(cycleDetails.start_date, cycleDetails.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const cycleTotalIssues =
cycleDetails.backlog_issues +
cycleDetails.unstarted_issues +
cycleDetails.started_issues +
cycleDetails.completed_issues +
cycleDetails.cancelled_issues;
const renderDate = cycleDetails.start_date || cycleDetails.end_date;
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100;
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
return ( return (
<> <>
<CycleCreateUpdateModal <CycleCreateUpdateModal
data={cycle} data={cycleDetails}
isOpen={updateModal} isOpen={updateModal}
handleClose={() => setUpdateModal(false)} handleClose={() => setUpdateModal(false)}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />
<CycleDeleteModal <CycleDeleteModal
cycle={cycle} cycle={cycleDetails}
isOpen={deleteModal} isOpen={deleteModal}
handleClose={() => setDeleteModal(false)} handleClose={() => setDeleteModal(false)}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
/> />
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="group flex h-16 w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90"> <div className="group flex h-16 w-full items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90">
<div className="flex w-full items-center gap-3 truncate"> <div className="flex w-full items-center gap-3 truncate">
<div className="flex items-center gap-4 truncate"> <div className="flex items-center gap-4 truncate">
@ -181,8 +183,8 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
<span className="flex-shrink-0"> <span className="flex-shrink-0">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" /> <CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
</span> </span>
<Tooltip tooltipContent={cycle.name} position="top"> <Tooltip tooltipContent={cycleDetails.name} position="top">
<span className="truncate text-base font-medium">{cycle.name}</span> <span className="truncate text-base font-medium">{cycleDetails.name}</span>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
@ -202,7 +204,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
}} }}
> >
{currentCycle.value === "current" {currentCycle.value === "current"
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
: `${currentCycle.label}`} : `${currentCycle.label}`}
</span> </span>
)} )}
@ -216,11 +218,11 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</span> </span>
)} )}
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}> <Tooltip tooltipContent={`${cycleDetails.assignees.length} Members`}>
<div className="flex w-16 cursor-default items-center justify-center gap-1"> <div className="flex w-16 cursor-default items-center justify-center gap-1">
{cycle.assignees.length > 0 ? ( {cycleDetails.assignees.length > 0 ? (
<AvatarGroup showTooltip={false}> <AvatarGroup showTooltip={false}>
{cycle.assignees.map((assignee) => ( {cycleDetails.assignees.map((assignee) => (
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} /> <Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} />
))} ))}
</AvatarGroup> </AvatarGroup>
@ -232,7 +234,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</div> </div>
</Tooltip> </Tooltip>
{isEditingAllowed && {isEditingAllowed &&
(cycle.is_favorite ? ( (cycleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}> <button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 fill-current text-amber-500" /> <Star className="h-3.5 w-3.5 fill-current text-amber-500" />
</button> </button>

View File

@ -6,18 +6,16 @@ import { useApplication } from "hooks/store";
import { CyclePeekOverview, CyclesListItem } from "components/cycles"; import { CyclePeekOverview, CyclesListItem } from "components/cycles";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// types
import { ICycle } from "types";
export interface ICyclesList { export interface ICyclesList {
cycles: ICycle[]; cycleIds: string[];
filter: string; filter: string;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
} }
export const CyclesList: FC<ICyclesList> = observer((props) => { export const CyclesList: FC<ICyclesList> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId } = props; const { cycleIds, filter, workspaceSlug, projectId } = props;
// store hooks // store hooks
const { const {
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
@ -26,14 +24,14 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
return ( return (
<> <>
{cycles ? ( {cycleIds ? (
<> <>
{cycles.length > 0 ? ( {cycleIds.length > 0 ? (
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
<div className="flex h-full w-full justify-between"> <div className="flex h-full w-full justify-between">
<div className="flex h-full w-full flex-col overflow-y-auto"> <div className="flex h-full w-full flex-col overflow-y-auto">
{cycles.map((cycle) => ( {cycleIds.map((cycleId) => (
<CyclesListItem cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} /> <CyclesListItem cycleId={cycleId} workspaceSlug={workspaceSlug} projectId={projectId} />
))} ))}
</div> </div>
<CyclePeekOverview <CyclePeekOverview

View File

@ -1,17 +1,16 @@
import { FC } from "react"; import { FC } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// store // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useCycle } from "hooks/store";
// components // components
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles"; import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles";
// ui components // ui components
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// types // types
import { TCycleLayout } from "types"; import { TCycleLayout, TCycleView } from "types";
export interface ICyclesView { export interface ICyclesView {
filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete"; filter: TCycleView;
layout: TCycleLayout; layout: TCycleLayout;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
@ -20,31 +19,24 @@ export interface ICyclesView {
export const CyclesView: FC<ICyclesView> = observer((props) => { export const CyclesView: FC<ICyclesView> = observer((props) => {
const { filter, layout, workspaceSlug, projectId, peekCycle } = props; const { filter, layout, workspaceSlug, projectId, peekCycle } = props;
// store hooks
// store const { projectCompletedCycles, projectDraftCycles, projectUpcomingCycles, projectAllCycles } = useCycle();
const { cycle: cycleStore } = useMobxStore();
// api call to fetch cycles list
useSWR(
workspaceSlug && projectId && filter ? `CYCLES_LIST_${projectId}_${filter}` : null,
workspaceSlug && projectId && filter ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null
);
const cyclesList = const cyclesList =
filter === "completed" filter === "completed"
? cycleStore.projectCompletedCycles ? projectCompletedCycles
: filter === "draft" : filter === "draft"
? cycleStore.projectDraftCycles ? projectDraftCycles
: filter === "upcoming" : filter === "upcoming"
? cycleStore.projectUpcomingCycles ? projectUpcomingCycles
: cycleStore.projectCycles; : projectAllCycles;
return ( return (
<> <>
{layout === "list" && ( {layout === "list" && (
<> <>
{cyclesList ? ( {cyclesList ? (
<CyclesList cycles={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} /> <CyclesList cycleIds={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
) : ( ) : (
<Loader className="space-y-4 p-8"> <Loader className="space-y-4 p-8">
<Loader.Item height="50px" /> <Loader.Item height="50px" />
@ -59,7 +51,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
<> <>
{cyclesList ? ( {cyclesList ? (
<CyclesBoard <CyclesBoard
cycles={cyclesList} cycleIds={cyclesList}
filter={filter} filter={filter}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -78,7 +70,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
{layout === "gantt" && ( {layout === "gantt" && (
<> <>
{cyclesList ? ( {cyclesList ? (
<CyclesListGanttChartView cycles={cyclesList} workspaceSlug={workspaceSlug} /> <CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />
) : ( ) : (
<Loader className="space-y-4"> <Loader className="space-y-4">
<Loader.Item height="50px" /> <Loader.Item height="50px" />

View File

@ -1,17 +1,15 @@
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
// next
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
// hooks
import { useApplication, useCycle } from "hooks/store";
import useToast from "hooks/use-toast";
// components // components
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// hooks
import useToast from "hooks/use-toast";
// types // types
import { ICycle } from "types"; import { ICycle } from "types";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
interface ICycleDelete { interface ICycleDelete {
cycle: ICycle; cycle: ICycle;
@ -23,56 +21,51 @@ interface ICycleDelete {
export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => { export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props; const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { postHogEventTracker },
} = useMobxStore();
// toast
const { setToastAlert } = useToast();
// states // states
const [loader, setLoader] = useState(false); const [loader, setLoader] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { cycleId, peekCycle } = router.query; const { cycleId, peekCycle } = router.query;
// store hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const { deleteCycle } = useCycle();
// toast alert
const { setToastAlert } = useToast();
const formSubmit = async () => { const formSubmit = async () => {
if (!cycle) return;
setLoader(true); setLoader(true);
if (cycle?.id) try {
try { await deleteCycle(workspaceSlug, projectId, cycle.id)
await cycleStore .then(() => {
.removeCycle(workspaceSlug, projectId, cycle?.id) setToastAlert({
.then(() => { type: "success",
setToastAlert({ title: "Success!",
type: "success", message: "Cycle deleted successfully.",
title: "Success!", });
message: "Cycle deleted successfully.", postHogEventTracker("CYCLE_DELETE", {
}); state: "SUCCESS",
postHogEventTracker("CYCLE_DELETE", { });
state: "SUCCESS", })
}); .catch(() => {
}) postHogEventTracker("CYCLE_DELETE", {
.catch(() => { state: "FAILED",
postHogEventTracker("CYCLE_DELETE", {
state: "FAILED",
});
}); });
if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
handleClose();
} catch (error) {
setToastAlert({
type: "error",
title: "Warning!",
message: "Something went wrong please try again later.",
}); });
}
else if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
handleClose();
} catch (error) {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Warning!", title: "Warning!",
message: "Something went wrong please try again later.", message: "Something went wrong please try again later.",
}); });
}
setLoader(false); setLoader(false);
}; };

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { KeyedMutator } from "swr"; import { KeyedMutator } from "swr";
// hooks // hooks
import { useUser } from "hooks/store"; import { useCycle, useUser } from "hooks/store";
// services // services
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
// components // components
@ -16,7 +16,7 @@ import { EUserWorkspaceRoles } from "constants/workspace";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
cycles: ICycle[]; cycleIds: string[];
mutateCycles?: KeyedMutator<ICycle[]>; mutateCycles?: KeyedMutator<ICycle[]>;
}; };
@ -24,7 +24,7 @@ type Props = {
const cycleService = new CycleService(); const cycleService = new CycleService();
export const CyclesListGanttChartView: FC<Props> = observer((props) => { export const CyclesListGanttChartView: FC<Props> = observer((props) => {
const { cycles, mutateCycles } = props; const { cycleIds, mutateCycles } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -32,6 +32,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { getCycleById } = useCycle();
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => { const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
@ -65,18 +66,21 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload); cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload);
}; };
const blockFormat = (blocks: ICycle[]) => const blockFormat = (blocks: (ICycle | null)[]) => {
blocks && blocks.length > 0 if (!blocks) return [];
? blocks
.filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)) const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date);
.map((block) => ({
data: block, const structuredBlocks = filteredBlocks.map((block) => ({
id: block.id, data: block,
sort_order: block.sort_order, id: block?.id ?? "",
start_date: new Date(block.start_date ?? ""), sort_order: block?.sort_order ?? 0,
target_date: new Date(block.end_date ?? ""), start_date: new Date(block?.start_date ?? ""),
})) target_date: new Date(block?.end_date ?? ""),
: []; }));
return structuredBlocks;
};
const isAllowed = const isAllowed =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
@ -86,7 +90,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
<GanttChartRoot <GanttChartRoot
title="Cycles" title="Cycles"
loaderTitle="Cycles" loaderTitle="Cycles"
blocks={cycles ? blockFormat(cycles) : null} blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null}
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
sidebarToRender={(props) => <CycleGanttSidebar {...props} />} sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />} blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />}

View File

@ -3,8 +3,8 @@ import { Dialog, Transition } from "@headlessui/react";
// services // services
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
// hooks // hooks
import { useApplication, useCycle } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CycleForm } from "components/cycles"; import { CycleForm } from "components/cycles";
// types // types
@ -23,21 +23,21 @@ const cycleService = new CycleService();
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => { export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const { isOpen, handleClose, data, workspaceSlug, projectId } = props; const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
// store
const {
cycle: cycleStore,
trackEvent: { postHogEventTracker },
} = useMobxStore();
// states // states
const [activeProject, setActiveProject] = useState<string>(projectId); const [activeProject, setActiveProject] = useState<string>(projectId);
// toast // store hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const { createCycle, updateCycleDetails } = useCycle();
// toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const createCycle = async (payload: Partial<ICycle>) => { const handleCreateCycle = async (payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project ?? projectId.toString(); const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore await createCycle(workspaceSlug, selectedProjectId, payload)
.createCycle(workspaceSlug, selectedProjectId, payload)
.then((res) => { .then((res) => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -61,11 +61,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
}); });
}; };
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => { const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project ?? projectId.toString(); const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
.patchCycle(workspaceSlug, selectedProjectId, cycleId, payload)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -116,8 +116,8 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
} }
if (isDateValid) { if (isDateValid) {
if (data) await updateCycle(data.id, payload); if (data) await handleUpdateCycle(data.id, payload);
else await createCycle(payload); else await handleCreateCycle(payload);
handleClose(); handleClose();
} else } else
setToastAlert({ setToastAlert({

View File

@ -3,11 +3,10 @@ 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";
import { Disclosure, Popover, Transition } from "@headlessui/react"; import { Disclosure, Popover, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import { CycleService } from "services/cycle.service"; import { CycleService } from "services/cycle.service";
// hooks // hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { SidebarProgressStats } from "components/core"; import { SidebarProgressStats } from "components/core";
@ -30,6 +29,8 @@ import {
} from "helpers/date-time.helper"; } from "helpers/date-time.helper";
// types // types
import { ICycle } from "types"; import { ICycle } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// fetch-keys // fetch-keys
import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_STATUS } from "constants/cycle";
@ -44,18 +45,21 @@ const cycleService = new CycleService();
// TODO: refactor the whole component // TODO: refactor the whole component
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => { export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { cycleId, handleClose } = props; const { cycleId, handleClose } = props;
// states
const [cycleDeleteModal, setCycleDeleteModal] = useState(false); const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query; const { workspaceSlug, projectId, peekCycle } = router.query;
// store hooks
const { const {
cycle: cycleDetailsStore, eventTracker: { setTrackElement },
trackEvent: { setTrackElement }, } = useApplication();
} = useMobxStore(); const {
membership: { currentProjectRole },
} = useUser();
const { getCycleById, updateCycleDetails } = useCycle();
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined; const cycleDetails = getCycleById(cycleId);
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -71,7 +75,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const submitChanges = (data: Partial<ICycle>) => { const submitChanges = (data: Partial<ICycle>) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
}; };
const handleCopyText = () => { const handleCopyText = () => {
@ -270,10 +274,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 +291,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 +319,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 +356,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 +382,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 +394,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 +420,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>

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

@ -1,9 +1,9 @@
import { useCallback, useState } from "react"; import { useCallback, 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";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useApplication, useLabel, useProject, useProjectState, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// components // components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
@ -25,27 +25,34 @@ import { EFilterType } from "store_legacy/issues/types";
import { EProjectStore } from "store_legacy/command-palette.store"; import { EProjectStore } from "store_legacy/command-palette.store";
export const CycleIssuesHeader: React.FC = observer(() => { export const CycleIssuesHeader: React.FC = observer(() => {
// states
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query as { const { workspaceSlug, projectId, cycleId } = router.query as {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
cycleId: string; cycleId: string;
}; };
// store hooks
const { const {
cycle: cycleStore, cycle: cycleStore,
projectIssuesFilter: projectIssueFiltersStore, projectIssuesFilter: projectIssueFiltersStore,
project: { currentProjectDetails },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectLabel: { projectLabels },
projectState: projectStateStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
cycleIssuesFilter: { issueFilters, updateFilters }, cycleIssuesFilter: { issueFilters, updateFilters },
user: { currentProjectRole },
} = useMobxStore(); } = useMobxStore();
const {
commandPalette: { toggleCreateIssueModal },
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout; const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout;
@ -156,7 +163,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>
@ -177,9 +187,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectLabels ?? undefined} labels={projectLabels}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId ?? ""] ?? undefined} states={projectStates}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">
@ -193,20 +203,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");
> toggleCreateIssueModal(true, EProjectStore.CYCLE);
Add Issue }}
</Button> size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)} )}
<button <button
type="button" type="button"

View File

@ -2,7 +2,8 @@ import { useCallback, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // hooks
import { useLabel, useUser } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues";
@ -16,6 +17,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_legacy/issues/types"; import { EFilterType } from "store_legacy/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 },
@ -28,19 +30,23 @@ type Props = {
export const GlobalIssuesHeader: React.FC<Props> = observer((props) => { export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const { activeLayout } = props; const { activeLayout } = props;
// states
const [createViewModal, setCreateViewModal] = useState(false); const [createViewModal, setCreateViewModal] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query as { workspaceSlug: string }; const { workspaceSlug } = router.query;
// store hooks
const { const {
workspace: { workspaceLabels },
workspaceMember: { workspaceMembers }, workspaceMember: { workspaceMembers },
project: { workspaceProjects }, project: { workspaceProjects },
workspaceGlobalIssuesFilter: { issueFilters, updateFilters }, workspaceGlobalIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
const {
membership: { currentWorkspaceRole },
} = useUser();
const {
workspace: { workspaceLabels },
} = useLabel();
const handleFiltersUpdate = useCallback( const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => { (key: keyof IIssueFilterOptions, value: string | string[]) => {
@ -56,7 +62,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
else newValues.push(value); else newValues.push(value);
} }
updateFilters(workspaceSlug, EFilterType.FILTERS, { [key]: newValues }); updateFilters(workspaceSlug.toString(), EFilterType.FILTERS, { [key]: newValues });
}, },
[workspaceSlug, issueFilters, updateFilters] [workspaceSlug, issueFilters, updateFilters]
); );
@ -64,7 +70,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const handleDisplayFilters = useCallback( const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => { (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
updateFilters(workspaceSlug, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter); updateFilters(workspaceSlug.toString(), EFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
}, },
[workspaceSlug, updateFilters] [workspaceSlug, updateFilters]
); );
@ -72,11 +78,13 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const handleDisplayProperties = useCallback( const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => { (property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
updateFilters(workspaceSlug, EFilterType.DISPLAY_PROPERTIES, property); updateFilters(workspaceSlug.toString(), EFilterType.DISPLAY_PROPERTIES, property);
}, },
[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 +150,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

@ -1,9 +1,9 @@
import { useCallback, useState } from "react"; import { useCallback, 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";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useApplication, useLabel, useProject, useProjectState, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// components // components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
@ -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";
@ -25,28 +25,33 @@ import { EFilterType } from "store_legacy/issues/types";
import { EProjectStore } from "store_legacy/command-palette.store"; import { EProjectStore } from "store_legacy/command-palette.store";
export const ModuleIssuesHeader: React.FC = observer(() => { export const ModuleIssuesHeader: React.FC = observer(() => {
// states
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query as { const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
moduleId: string; moduleId: string;
}; };
// store hooks
const { const {
module: moduleStore, module: moduleStore,
project: projectStore,
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
projectLabel: { projectLabels },
moduleIssuesFilter: { issueFilters, updateFilters }, moduleIssuesFilter: { issueFilters, updateFilters },
user: { currentProjectRole },
} = useMobxStore(); } = useMobxStore();
const {
const { currentProjectDetails } = projectStore; commandPalette: { toggleCreateIssueModal },
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const {
project: { projectLabels },
} = useLabel();
const { projectStates } = useProjectState();
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
@ -144,7 +149,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)}
</> </>
} }
@ -157,7 +162,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>
@ -178,9 +186,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectLabels ?? undefined} labels={projectLabels}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId ?? ""] ?? undefined} states={projectStates}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">
@ -194,20 +202,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");
> toggleCreateIssueModal(true, EProjectStore.MODULE);
Add Issue }}
</Button> size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)} )}
<button <button
type="button" type="button"

View File

@ -1,32 +1,35 @@
import { FC } from "react"; import { FC } 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 { ArrowLeft } from "lucide-react";
// hooks // hooks
import { useLabel, useProject, useProjectState } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
// ui // ui
import { Breadcrumbs, LayersIcon } from "@plane/ui"; import { Breadcrumbs, LayersIcon } from "@plane/ui";
// icons
import { ArrowLeft } from "lucide-react";
// components // components
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// types // types
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "types"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "types";
// helper
import { renderEmoji } from "helpers/emoji.helper";
export const ProjectArchivedIssuesHeader: FC = observer(() => { export const ProjectArchivedIssuesHeader: FC = observer(() => {
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks
const { const {
project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
archivedIssueFilters: archivedIssueFiltersStore, archivedIssueFilters: archivedIssueFiltersStore,
projectState: projectStateStore,
} = useMobxStore(); } = useMobxStore();
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
// for archived issues list layout is the only option // for archived issues list layout is the only option
const activeLayout = "list"; const activeLayout = "list";
@ -118,9 +121,9 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.archived_issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.archived_issues[activeLayout] : undefined
} }
labels={projectLabels ?? undefined} labels={projectLabels}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStates}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">

View File

@ -2,6 +2,7 @@ import { FC, 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";
// hooks // hooks
import { useLabel, useProject, useProjectState } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
@ -14,16 +15,19 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
export const ProjectDraftIssueHeader: FC = observer(() => { export const ProjectDraftIssueHeader: FC = observer(() => {
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
// store hooks
const { const {
project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore,
projectDraftIssuesFilter: { issueFilters, updateFilters }, projectDraftIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
@ -112,9 +116,9 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectLabels ?? undefined} labels={projectLabels}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId ?? ""] ?? undefined} states={projectStates}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">

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

@ -2,7 +2,8 @@ import { 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";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// mobx store // hooks
import { useApplication, useLabel, useProject, useProjectState, useUser } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
@ -21,24 +22,31 @@ import { EFilterType } from "store_legacy/issues/types";
import { EProjectStore } from "store_legacy/command-palette.store"; import { EProjectStore } from "store_legacy/command-palette.store";
export const ProjectViewIssuesHeader: React.FC = observer(() => { export const ProjectViewIssuesHeader: React.FC = observer(() => {
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query as { const { workspaceSlug, projectId, viewId } = router.query as {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
viewId: string; viewId: string;
}; };
// store hooks
const { const {
project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore,
projectViews: projectViewsStore, projectViews: projectViewsStore,
viewIssuesFilter: { issueFilters, updateFilters }, viewIssuesFilter: { issueFilters, updateFilters },
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
user: { currentProjectRole },
} = useMobxStore(); } = useMobxStore();
const {
commandPalette: { toggleCreateIssueModal },
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
@ -139,7 +147,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>
@ -153,16 +164,17 @@ 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}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectLabels ?? undefined} labels={projectLabels}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId ?? ""] ?? undefined} states={projectStates}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">
@ -176,18 +188,18 @@ 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");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW); toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
}} }}
size="sm" size="sm"
prependIcon={<Plus />} prependIcon={<Plus />}
> >
Add Issue Add Issue
</Button> </Button>
} )}
</div> </div>
</div> </div>
); );

View File

@ -2,11 +2,13 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// hooks // hooks
import { useApplication, useProject } from "hooks/store"; import { useApplication, useProject, useUser } from "hooks/store";
// components // components
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
@ -16,8 +18,14 @@ export const ProjectViewsHeader: React.FC = observer(() => {
const { const {
commandPalette: { toggleCreateViewModal }, commandPalette: { toggleCreateViewModal },
} = useApplication(); } = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
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">
@ -52,18 +60,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={() => toggleCreateViewModal(true)} prependIcon={<Plus className="h-3.5 w-3.5 stroke-2" />}
> onClick={() => toggleCreateViewModal(true)}
Create View >
</Button> Create View
</Button>
</div>
</div> </div>
</div> )}
</div> </div>
</> </>
); );

View File

@ -1,9 +1,11 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Search, Plus, Briefcase } from "lucide-react"; import { Search, Plus, Briefcase } from "lucide-react";
// hooks // hooks
import { useApplication, useProject } from "hooks/store"; import { useApplication, useProject, useUser } from "hooks/store";
// ui // ui
import { Breadcrumbs, Button } from "@plane/ui"; import { Breadcrumbs, Button } from "@plane/ui";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectsHeader = observer(() => { export const ProjectsHeader = observer(() => {
// store hooks // store hooks
@ -11,8 +13,13 @@ export const ProjectsHeader = observer(() => {
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
eventTracker: { setTrackElement }, eventTracker: { setTrackElement },
} = useApplication(); } = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
const { workspaceProjects, searchQuery, setSearchQuery } = useProject(); const { workspaceProjects, searchQuery, setSearchQuery } = useProject();
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">
@ -38,17 +45,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

@ -4,9 +4,9 @@ import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { AlertTriangle, CheckCircle2, Clock, Copy, ExternalLink, Inbox, XCircle } from "lucide-react"; import { AlertTriangle, CheckCircle2, Clock, Copy, ExternalLink, Inbox, XCircle } from "lucide-react";
// hooks
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { useProjectState, useUser } from "hooks/store";
// components // components
import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction, IssueUpdateStatus } from "components/issues"; import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction, IssueUpdateStatus } from "components/issues";
import { InboxIssueActivity } from "components/inbox"; import { InboxIssueActivity } from "components/inbox";
@ -28,19 +28,19 @@ const defaultValues: Partial<IInboxIssue> = {
}; };
export const InboxMainContent: React.FC = observer(() => { export const InboxMainContent: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
// states // states
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
// router
const router = useRouter();
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
// store hooks
const { inboxIssues: inboxIssuesStore, inboxIssueDetails: inboxIssueDetailsStore } = useMobxStore();
const { const {
inboxIssues: inboxIssuesStore, currentUser,
inboxIssueDetails: inboxIssueDetailsStore, membership: { currentProjectRole },
user: { currentUser, currentProjectRole }, } = useUser();
projectState: { states }, const { projectStates } = useProjectState();
} = useMobxStore(); // form info
const { reset, control, watch } = useForm<IIssue>({ const { reset, control, watch } = useForm<IIssue>({
defaultValues, defaultValues,
}); });
@ -60,9 +60,7 @@ export const InboxMainContent: React.FC = observer(() => {
const issuesList = inboxId ? inboxIssuesStore.inboxIssues[inboxId.toString()] : undefined; const issuesList = inboxId ? inboxIssuesStore.inboxIssues[inboxId.toString()] : undefined;
const issueDetails = inboxIssueId ? inboxIssueDetailsStore.issueDetails[inboxIssueId.toString()] : undefined; const issueDetails = inboxIssueId ? inboxIssueDetailsStore.issueDetails[inboxIssueId.toString()] : undefined;
const currentIssueState = projectId const currentIssueState = projectStates?.find((s) => s.id === issueDetails?.state);
? states[projectId.toString()]?.find((s) => s.id === issueDetails?.state)
: undefined;
const submitChanges = useCallback( const submitChanges = useCallback(
async (formData: Partial<IInboxIssue>) => { async (formData: Partial<IInboxIssue>) => {
@ -165,16 +163,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 +223,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

@ -4,22 +4,22 @@ import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { RichTextEditorWithRef } from "@plane/rich-text-editor"; import { RichTextEditorWithRef } from "@plane/rich-text-editor";
import { Sparkle } from "lucide-react";
// mobx store // hooks
import { useApplication, useWorkspace } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast";
import useEditorSuggestions from "hooks/use-editor-suggestions";
// services // services
import { FileService } from "services/file.service"; import { FileService } from "services/file.service";
import { AIService } from "services/ai.service";
// components // components
import { IssuePrioritySelect } from "components/issues/select"; import { IssuePrioritySelect } from "components/issues/select";
import { GptAssistantModal } from "components/core";
// ui // ui
import { Button, Input, ToggleSwitch } from "@plane/ui"; import { Button, Input, ToggleSwitch } from "@plane/ui";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import useEditorSuggestions from "hooks/use-editor-suggestions";
import { GptAssistantModal } from "components/core";
import { Sparkle } from "lucide-react";
import useToast from "hooks/use-toast";
import { AIService } from "services/ai.service";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -40,30 +40,29 @@ const fileService = new FileService();
export const CreateInboxIssueModal: React.FC<Props> = observer((props) => { export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
const { isOpen, onClose } = props; const { isOpen, onClose } = props;
// states // states
const [createMore, setCreateMore] = useState(false); const [createMore, setCreateMore] = useState(false);
const [gptAssistantModal, setGptAssistantModal] = useState(false); const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
// refs
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
// toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const editorSuggestion = useEditorSuggestions(); const editorSuggestion = useEditorSuggestions();
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, inboxId } = router.query as { const { workspaceSlug, projectId, inboxId } = router.query as {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
inboxId: string; inboxId: string;
}; };
// store hooks
const { inboxIssueDetails: inboxIssueDetailsStore } = useMobxStore();
const { const {
inboxIssueDetails: inboxIssueDetailsStore, config: { envConfig },
trackEvent: { postHogEventTracker }, eventTracker: { postHogEventTracker },
appConfig: { envConfig }, } = useApplication();
workspace: { currentWorkspace }, const { currentWorkspace } = useWorkspace();
} = useMobxStore();
const { const {
control, control,

View File

@ -2,10 +2,9 @@ import React, { 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 { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useApplication, useWorkspace } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// icons // icons
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
@ -21,16 +20,17 @@ type Props = {
}; };
export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClose, data }) => { export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClose, data }) => {
// states
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, inboxId } = router.query; const { workspaceSlug, projectId, inboxId } = router.query;
// store hooks
const { inboxIssueDetails: inboxIssueDetailsStore } = useMobxStore();
const { const {
inboxIssueDetails: inboxIssueDetailsStore, eventTracker: { postHogEventTracker },
trackEvent: { postHogEventTracker }, } = useApplication();
workspace: { currentWorkspace }, const { currentWorkspace } = useWorkspace();
} = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();

View File

@ -135,7 +135,7 @@ 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"
hasError={Boolean(errors?.description)} hasError={Boolean(errors?.description)}
role="textbox" role="textbox"
disabled={!isAllowed} disabled={!isAllowed}

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

@ -15,10 +15,11 @@ 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();
@ -52,7 +53,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
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];

View File

@ -26,6 +26,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> = {
@ -56,7 +57,8 @@ 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();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -142,6 +144,11 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
} }
}; };
const handleOpen = () => {
setIsOpen(true);
if (onOpen) onOpen();
};
return ( return (
<> <>
{isOpen && ( {isOpen && (
@ -165,7 +172,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

@ -1,9 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useApplication, useUser } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
@ -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_legacy/command-palette.store"; import { EProjectStore } from "store_legacy/command-palette.store";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = { type Props = {
workspaceSlug: string | undefined; workspaceSlug: string | undefined;
@ -26,12 +28,15 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleId } = props; const { workspaceSlug, projectId, cycleId } = props;
// states // states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
// store hooks
const { cycleIssues: cycleIssueStore } = useMobxStore();
const { const {
cycleIssues: cycleIssueStore, commandPalette: { toggleCreateIssueModal },
commandPalette: commandPaletteStore, eventTracker: { setTrackElement },
trackEvent: { setTrackElement }, } = useApplication();
} = useMobxStore(); const {
membership: { currentProjectRole: userRole },
} = useUser();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -49,6 +54,8 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
}); });
}; };
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<> <>
<ExistingIssuesListModal <ExistingIssuesListModal
@ -67,7 +74,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />, icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => { onClick: () => {
setTrackElement("CYCLE_EMPTY_STATE"); setTrackElement("CYCLE_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE); toggleCreateIssueModal(true, EProjectStore.CYCLE);
}, },
}} }}
secondaryButton={ secondaryButton={
@ -75,10 +82,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

@ -1,15 +1,21 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
// hooks
import { useApplication, useUser } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast";
// components // components
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
import { ExistingIssuesListModal } from "components/core";
// ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// assets // assets
import emptyIssue from "public/empty-state/issue.svg"; import emptyIssue from "public/empty-state/issue.svg";
import { ExistingIssuesListModal } from "components/core"; // types
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
import { ISearchIssueResponse } from "types"; import { ISearchIssueResponse } from "types";
import useToast from "hooks/use-toast"; // constants
import { useState } from "react"; import { EUserWorkspaceRoles } from "constants/workspace";
type Props = { type Props = {
workspaceSlug: string | undefined; workspaceSlug: string | undefined;
@ -21,13 +27,16 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, moduleId } = props; const { workspaceSlug, projectId, moduleId } = props;
// states // states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
// store hooks
const { moduleIssues: moduleIssueStore } = useMobxStore();
const { const {
moduleIssues: moduleIssueStore, commandPalette: { toggleCreateIssueModal },
commandPalette: commandPaletteStore, eventTracker: { setTrackElement },
trackEvent: { setTrackElement }, } = useApplication();
} = useMobxStore(); const {
membership: { currentProjectRole: userRole },
} = useUser();
// toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
@ -44,6 +53,8 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
); );
}; };
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<> <>
<ExistingIssuesListModal <ExistingIssuesListModal
@ -62,7 +73,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />, icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => { onClick: () => {
setTrackElement("MODULE_EMPTY_STATE"); setTrackElement("MODULE_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true); toggleCreateIssueModal(true);
}, },
}} }}
secondaryButton={ secondaryButton={
@ -70,10 +81,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

@ -1,9 +1,11 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
// hooks // hooks
import { useApplication } from "hooks/store"; import { useApplication, useUser } from "hooks/store";
// 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_legacy/command-palette.store"; import { EProjectStore } from "store_legacy/command-palette.store";
@ -14,6 +16,11 @@ export const ProjectEmptyState: React.FC = observer(() => {
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
eventTracker: { setTrackElement }, eventTracker: { setTrackElement },
} = useApplication(); } = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
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">
@ -35,6 +42,7 @@ export const ProjectEmptyState: React.FC = observer(() => {
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT); commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
}, },
}} }}
disabled={!isEditingAllowed}
/> />
</div> </div>
); );

View File

@ -1,5 +1,7 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
// hooks
import { useUser } from "hooks/store";
// components // components
import { import {
AppliedDateFilters, AppliedDateFilters,
@ -10,12 +12,12 @@ import {
AppliedStateFilters, AppliedStateFilters,
AppliedStateGroupFilters, AppliedStateGroupFilters,
} from "components/issues"; } from "components/issues";
// icons
import { X } from "lucide-react";
// helpers // helpers
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;
@ -32,11 +34,17 @@ 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;
// store hooks
const {
membership: { currentProjectRole },
} = useUser();
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

@ -105,14 +105,11 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const displayProperties = issuesFilterStore?.issueFilters?.displayProperties || null; const displayProperties = issuesFilterStore?.issueFilters?.displayProperties || null;
const sub_group_by: string | null = displayFilters?.sub_group_by || null; const sub_group_by: string | null = displayFilters?.sub_group_by || null;
const group_by: string | null = displayFilters?.group_by || null; const group_by: string | null = displayFilters?.group_by || null;
const order_by: string | null = displayFilters?.order_by || null;
const userDisplayFilters = displayFilters || null; const userDisplayFilters = displayFilters || null;
const currentKanBanView: "swimlanes" | "default" = sub_group_by ? "swimlanes" : "default"; const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {}; const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
@ -256,48 +253,25 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
</Droppable> </Droppable>
</div> </div>
{currentKanBanView === "default" ? ( <KanBanView
<KanBan issues={issues}
issues={issues} issueIds={issueIds}
issueIds={issueIds} sub_group_by={sub_group_by}
sub_group_by={sub_group_by} group_by={group_by}
group_by={group_by} handleIssues={handleIssues}
handleIssues={handleIssues} quickActions={renderQuickActions}
quickActions={renderQuickActions} displayProperties={displayProperties}
displayProperties={displayProperties} kanBanToggle={kanbanViewStore?.kanBanToggle}
kanBanToggle={kanbanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle}
handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickAdd}
enableQuickIssueCreate={enableQuickAdd} showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true} quickAddCallback={issueStore?.quickAddIssue}
quickAddCallback={issueStore?.quickAddIssue} viewId={viewId}
viewId={viewId} disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed} canEditProperties={canEditProperties}
canEditProperties={canEditProperties} currentStore={currentStore}
currentStore={currentStore} addIssuesToView={addIssuesToView}
addIssuesToView={addIssuesToView} />
/>
) : (
<KanBanSwimLanes
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={renderQuickActions}
displayProperties={displayProperties}
kanBanToggle={kanbanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
enableQuickIssueCreate={enableQuickAdd}
currentStore={currentStore}
quickAddCallback={issueStore?.quickAddIssue}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
)}
</DragDropContext> </DragDropContext>
</div> </div>

View File

@ -1,4 +1,3 @@
import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";

View File

@ -1,4 +1,3 @@
import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
//mobx //mobx
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
@ -54,7 +53,6 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
issues: IIssueResponse; issues: IIssueResponse;
issueIds: any; issueIds: any;
order_by: string | null;
showEmptyGroup: boolean; showEmptyGroup: boolean;
handleIssues: (issue: IIssue, action: EIssueActions) => void; handleIssues: (issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
@ -73,6 +71,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
data: IIssue, data: IIssue,
viewId?: string viewId?: string
) => Promise<IIssue | undefined>; ) => Promise<IIssue | undefined>;
viewId?: string;
} }
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => { const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const { const {
@ -91,6 +90,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
canEditProperties, canEditProperties,
addIssuesToView, addIssuesToView,
quickAddCallback, quickAddCallback,
viewId,
} = props; } = props;
const calculateIssueCount = (column_id: string) => { const calculateIssueCount = (column_id: string) => {
@ -139,6 +139,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
</div> </div>
)} )}
@ -153,7 +154,6 @@ export interface IKanBanSwimLanes {
issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues; issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
order_by: string | null;
handleIssues: (issue: IIssue, action: EIssueActions) => void; handleIssues: (issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null; displayProperties: IIssueDisplayProperties | null;
@ -171,6 +171,7 @@ export interface IKanBanSwimLanes {
data: IIssue, data: IIssue,
viewId?: string viewId?: string
) => Promise<IIssue | undefined>; ) => Promise<IIssue | undefined>;
viewId?: string;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
} }
@ -180,7 +181,6 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issueIds, issueIds,
sub_group_by, sub_group_by,
group_by, group_by,
order_by,
handleIssues, handleIssues,
quickActions, quickActions,
displayProperties, displayProperties,
@ -193,6 +193,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
canEditProperties, canEditProperties,
addIssuesToView, addIssuesToView,
quickAddCallback, quickAddCallback,
viewId,
} = props; } = props;
const { project, projectLabel, projectMember, projectState } = useMobxStore(); const { project, projectLabel, projectMember, projectState } = useMobxStore();
@ -228,7 +229,6 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issueIds={issueIds} issueIds={issueIds}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
order_by={order_by}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
@ -241,6 +241,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
</div> </div>

View File

@ -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) => {

View File

@ -1,15 +1,14 @@
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Check, ChevronDown, Search, Tags } from "lucide-react";
// hooks
import { useApplication, useLabel } from "hooks/store";
// components // components
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
import { Check, ChevronDown, Search, Tags } from "lucide-react";
// types // types
import { Placement } from "@popperjs/core"; import { Placement } from "@popperjs/core";
import { RootStore } from "store_legacy/root";
import { IIssueLabel } from "types"; import { IIssueLabel } from "types";
export interface IIssuePropertyLabels { export interface IIssuePropertyLabels {
@ -44,18 +43,19 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
noLabelBorder = false, noLabelBorder = false,
placeholderText, placeholderText,
} = props; } = props;
// states
const {
workspace: workspaceStore,
projectLabel: { fetchProjectLabels, labels },
}: RootStore = useMobxStore();
const workspaceSlug = workspaceStore?.workspaceSlug;
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
// popper-js refs
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);
const [isLoading, setIsLoading] = useState<Boolean>(false); const [isLoading, setIsLoading] = useState<Boolean>(false);
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const {
project: { fetchProjectLabels, projectLabels: storeLabels },
} = useLabel();
const fetchLabels = () => { const fetchLabels = () => {
setIsLoading(true); setIsLoading(true);
@ -65,7 +65,6 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
if (!value) return null; if (!value) return null;
let projectLabels: IIssueLabel[] = defaultOptions; let projectLabels: IIssueLabel[] = defaultOptions;
const storeLabels = projectId && labels ? labels[projectId] : [];
if (storeLabels && storeLabels.length > 0) projectLabels = storeLabels; if (storeLabels && storeLabels.length > 0) projectLabels = storeLabels;
const options = projectLabels.map((label) => ({ const options = projectLabels.map((label) => ({

View File

@ -2,8 +2,9 @@ import React, { 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";
// mobx store // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { useCycle } from "hooks/store";
// components // components
import { import {
CycleAppliedFiltersRoot, CycleAppliedFiltersRoot,
@ -29,12 +30,13 @@ export const CycleLayoutRoot: React.FC = observer(() => {
projectId: string; projectId: string;
cycleId: string; cycleId: string;
}; };
// store hooks
const { const {
cycle: cycleStore, cycle: cycleStore,
cycleIssues: { loader, getIssues, fetchIssues }, cycleIssues: { loader, getIssues, fetchIssues },
cycleIssuesFilter: { issueFilters, fetchFilters }, cycleIssuesFilter: { issueFilters, fetchFilters },
} = useMobxStore(); } = useMobxStore();
const { getCycleById } = useCycle();
useSWR( useSWR(
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null, workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
@ -48,7 +50,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined; const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
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)

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

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

@ -6,10 +6,12 @@ import { IssuePropertyState } from "../../properties";
import useSubIssue from "hooks/use-sub-issue"; import useSubIssue from "hooks/use-sub-issue";
// types // types
import { IIssue, IState } from "types"; import { IIssue, IState } from "types";
import { mutate } from "swr";
import { SUB_ISSUES } from "constants/fetch-keys";
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 +22,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 +30,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

@ -3,12 +3,11 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { MinusCircle } from "lucide-react"; import { MinusCircle } from "lucide-react";
// mobx store // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useApplication, useProject, useProjectState, useUser, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast";
// services // services
import { IssueService, IssueCommentService } from "services/issue"; import { IssueService, IssueCommentService } from "services/issue";
// hooks
import useToast from "hooks/use-toast";
// components // components
import { import {
AddComment, AddComment,
@ -49,19 +48,19 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// mobx store
const { const {
user: { currentUser, currentProjectRole }, eventTracker: { postHogEventTracker },
project: projectStore, } = useApplication();
projectState: { states }, const {
trackEvent: { postHogEventTracker }, currentUser,
workspace: { currentWorkspace }, membership: { currentProjectRole },
} = useMobxStore(); } = useUser();
const { currentWorkspace } = useWorkspace();
const { getProjectById } = useProject();
const { projectStates } = useProjectState();
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined; const projectDetails = projectId ? getProjectById(projectId.toString()) : null;
const currentIssueState = projectId const currentIssueState = projectStates?.find((s) => s.id === issueDetails.state);
? states[projectId.toString()]?.find((s) => s.id === issueDetails.state)
: undefined;
const { data: siblingIssues } = useSWR( const { data: siblingIssues } = useSWR(
workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null, workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
@ -94,7 +93,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
{ {
isGrouping: true, isGrouping: true,
groupType: "Workspace_metrics", groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!, groupId: currentWorkspace?.id!,
} }
); );
}); });
@ -117,7 +116,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
{ {
isGrouping: true, isGrouping: true,
groupType: "Workspace_metrics", groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!, groupId: currentWorkspace?.id!,
} }
); );
}); });
@ -139,7 +138,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
{ {
isGrouping: true, isGrouping: true,
groupType: "Workspace_metrics", groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!, groupId: currentWorkspace?.id!,
} }
); );
}) })
@ -216,7 +215,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"
@ -260,12 +259,12 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
activity={issueActivity} activity={issueActivity}
handleCommentUpdate={handleCommentUpdate} handleCommentUpdate={handleCommentUpdate}
handleCommentDelete={handleCommentDelete} handleCommentDelete={handleCommentDelete}
showAccessSpecifier={projectDetails && projectDetails.is_deployed} showAccessSpecifier={Boolean(projectDetails && projectDetails.is_deployed)}
/> />
<AddComment <AddComment
onSubmit={handleAddComment} onSubmit={handleAddComment}
disabled={uneditable} disabled={uneditable}
showAccessSpecifier={projectDetails && projectDetails.is_deployed} showAccessSpecifier={Boolean(projectDetails && projectDetails.is_deployed)}
/> />
</div> </div>
</> </>

View File

@ -3,13 +3,13 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { mutate } from "swr"; import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { IssueDraftService } from "services/issue";
// hooks // hooks
import { useApplication, useUser, useWorkspace } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// services
import { IssueDraftService } from "services/issue";
// components // components
import { IssueForm, ConfirmIssueDiscard } from "components/issues"; import { IssueForm, ConfirmIssueDiscard } from "components/issues";
// types // types
@ -57,14 +57,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
handleSubmit, handleSubmit,
currentStore = EProjectStore.PROJECT, currentStore = EProjectStore.PROJECT,
} = props; } = props;
// states // states
const [createMore, setCreateMore] = useState(false); const [createMore, setCreateMore] = useState(false);
const [formDirtyState, setFormDirtyState] = useState<any>(null); const [formDirtyState, setFormDirtyState] = useState<any>(null);
const [showConfirmDiscard, setShowConfirmDiscard] = useState(false); const [showConfirmDiscard, setShowConfirmDiscard] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null); const [activeProject, setActiveProject] = useState<string | null>(null);
const [prePopulateData, setPreloadedData] = useState<Partial<IIssue>>({}); const [prePopulateData, setPreloadedData] = useState<Partial<IIssue>>({});
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query as { const { workspaceSlug, projectId, cycleId, moduleId } = router.query as {
workspaceSlug: string; workspaceSlug: string;
@ -72,7 +71,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
cycleId: string | undefined; cycleId: string | undefined;
moduleId: string | undefined; moduleId: string | undefined;
}; };
// store hooks
const { const {
project: projectStore, project: projectStore,
projectIssues: projectIssueStore, projectIssues: projectIssueStore,
@ -80,12 +79,12 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
workspaceProfileIssues: profileIssueStore, workspaceProfileIssues: profileIssueStore,
cycleIssues: cycleIssueStore, cycleIssues: cycleIssueStore,
moduleIssues: moduleIssueStore, moduleIssues: moduleIssueStore,
user: userStore,
trackEvent: { postHogEventTracker },
workspace: { currentWorkspace },
} = useMobxStore(); } = useMobxStore();
const {
const user = userStore.currentUser; eventTracker: { postHogEventTracker },
} = useApplication();
const { currentUser } = useUser();
const { currentWorkspace } = useWorkspace();
const issueStores = { const issueStores = {
[EProjectStore.PROJECT]: { [EProjectStore.PROJECT]: {
@ -100,7 +99,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
}, },
[EProjectStore.PROFILE]: { [EProjectStore.PROFILE]: {
store: profileIssueStore, store: profileIssueStore,
dataIdToUpdate: user?.id || undefined, dataIdToUpdate: currentUser?.id || undefined,
viewId: undefined, viewId: undefined,
}, },
[EProjectStore.CYCLE]: { [EProjectStore.CYCLE]: {
@ -150,10 +149,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
setPreloadedData((prevData) => ({ setPreloadedData((prevData) => ({
...(prevData ?? {}), ...(prevData ?? {}),
...prePopulateDataProps, ...prePopulateDataProps,
assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""], assignees: prePopulateDataProps?.assignees ?? [currentUser?.id ?? ""],
})); }));
} }
}, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]); }, [prePopulateDataProps, cycleId, moduleId, router.asPath, currentUser?.id]);
/** /**
* *
@ -260,7 +259,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
{ {
isGrouping: true, isGrouping: true,
groupType: "Workspace_metrics", groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!, groupId: currentWorkspace?.id!,
} }
); );
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
@ -280,7 +279,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
{ {
isGrouping: true, isGrouping: true,
groupType: "Workspace_metrics", groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!, groupId: currentWorkspace?.id!,
} }
); );
}); });
@ -289,7 +288,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
}; };
const createDraftIssue = async () => { const createDraftIssue = async () => {
if (!workspaceSlug || !activeProject || !user) return; if (!workspaceSlug || !activeProject || !currentUser) return;
const payload: Partial<IIssue> = { const payload: Partial<IIssue> = {
...formDirtyState, ...formDirtyState,
@ -308,7 +307,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
setFormDirtyState(null); setFormDirtyState(null);
setShowConfirmDiscard(false); setShowConfirmDiscard(false);
if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string)); if (payload.assignees?.some((assignee) => assignee === currentUser?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
}) })
@ -343,7 +343,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
{ {
isGrouping: true, isGrouping: true,
groupType: "Workspace_metrics", groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!, groupId: currentWorkspace?.id!,
} }
); );
}) })
@ -361,7 +361,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
{ {
isGrouping: true, isGrouping: true,
groupType: "Workspace_metrics", groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!, groupId: currentWorkspace?.id!,
} }
); );
}); });

View File

@ -1,12 +1,12 @@
import { FC, useState } from "react"; import { FC, 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";
// mobx store import { CalendarDays, Link2, Plus, Signal, Tag, Triangle, LayoutPanelTop } from "lucide-react";
// hooks
import { useProject, useUser } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// ui icons // ui icons
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon } from "@plane/ui"; import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon } from "@plane/ui";
import { CalendarDays, Link2, Plus, Signal, Tag, Triangle, LayoutPanelTop } from "lucide-react";
import { import {
SidebarAssigneeSelect, SidebarAssigneeSelect,
SidebarCycleSelect, SidebarCycleSelect,
@ -39,13 +39,15 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
// states // states
const [linkModal, setLinkModal] = useState(false); const [linkModal, setLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
// store hooks
const { const {
user: { currentProjectRole },
issueDetail: { fetchPeekIssueDetails }, issueDetail: { fetchPeekIssueDetails },
project: { getProjectById },
} = useMobxStore(); } = useMobxStore();
const {
membership: { currentProjectRole },
} = useUser();
const { getProjectById } = useProject();
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;

View File

@ -60,7 +60,9 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, disabl
) : ( ) : (
<button <button
type="button" type="button"
className="rounded bg-custom-background-80 px-2.5 py-0.5 text-xs text-custom-text-200" className={`rounded bg-custom-background-80 px-2.5 py-0.5 text-xs text-custom-text-200 ${
disabled ? "cursor-not-allowed" : ""
}`}
> >
No assignees No assignees
</button> </button>

View File

@ -4,15 +4,13 @@ import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { TwitterPicker } from "react-color"; import { TwitterPicker } from "react-color";
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// mobx store import { Plus, X } from "lucide-react";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useLabel } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { Input } from "@plane/ui"; import { Input } from "@plane/ui";
import { IssueLabelSelect } from "../select"; import { IssueLabelSelect } from "../select";
// icons
import { Plus, X } from "lucide-react";
// types // types
import { IIssue, IIssueLabel } from "types"; import { IIssue, IIssueLabel } from "types";
@ -40,8 +38,8 @@ export const SidebarLabelSelect: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// mobx store // mobx store
const { const {
projectLabel: { projectLabels, createLabel }, project: { projectLabels, createLabel },
} = useMobxStore(); } = useLabel();
// form info // form info
const { const {
handleSubmit, handleSubmit,

View File

@ -4,6 +4,8 @@ import { useRouter } from "next/router";
// components // components
import { ParentIssuesListModal } from "components/issues"; import { ParentIssuesListModal } from "components/issues";
// icons
import { X } from "lucide-react";
// types // types
import { IIssue, ISearchIssueResponse } from "types"; import { IIssue, ISearchIssueResponse } from "types";
@ -32,12 +34,20 @@ export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, d
issueId={issueId as string} issueId={issueId as string}
projectId={projectId as string} projectId={projectId as string}
/> />
<button <button
type="button" type="button"
className={`rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${ className={`flex items-center gap-2 rounded bg-custom-background-80 px-2.5 py-0.5 text-xs max-w-max" ${
disabled ? "cursor-not-allowed" : "cursor-pointer " disabled ? "cursor-not-allowed" : "cursor-pointer "
}`} }`}
onClick={() => setIsParentModalOpen(true)} onClick={() => {
if (issueDetails?.parent) {
onChange("");
setSelectedParentIssue(null);
} else {
setIsParentModalOpen(true);
}
}}
disabled={disabled} disabled={disabled}
> >
{selectedParentIssue && issueDetails?.parent ? ( {selectedParentIssue && issueDetails?.parent ? (
@ -47,6 +57,7 @@ export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, d
) : ( ) : (
<span className="text-custom-text-200">Select issue</span> <span className="text-custom-text-200">Select issue</span>
)} )}
{issueDetails?.parent && <X className="h-2.5 w-2.5" />}
</button> </button>
</> </>
); );

View File

@ -3,9 +3,10 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { mutate } from "swr"; import { mutate } from "swr";
import { Controller, UseFormWatch } from "react-hook-form"; import { Controller, UseFormWatch } from "react-hook-form";
// mobx store import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, LayoutPanelTop } from "lucide-react";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useProjectState, useUser } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useUserIssueNotificationSubscription from "hooks/use-issue-notification-subscription"; import useUserIssueNotificationSubscription from "hooks/use-issue-notification-subscription";
import useEstimateOption from "hooks/use-estimate-option"; import useEstimateOption from "hooks/use-estimate-option";
@ -32,7 +33,6 @@ import {
// ui // ui
import { CustomDatePicker } from "components/ui"; import { CustomDatePicker } from "components/ui";
// icons // icons
import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, LayoutPanelTop } from "lucide-react";
import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
@ -75,18 +75,21 @@ const moduleService = new ModuleService();
export const IssueDetailsSidebar: React.FC<Props> = observer((props) => { export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const { control, submitChanges, issueDetail, watch: watchIssue, fieldsToShow = ["all"], uneditable = false } = props; const { control, submitChanges, issueDetail, watch: watchIssue, fieldsToShow = ["all"], uneditable = false } = props;
// states
const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [linkModal, setLinkModal] = useState(false); const [linkModal, setLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
// store hooks
const { const {
user: { currentUser, currentProjectRole },
projectState: { states },
projectIssues: { removeIssue }, projectIssues: { removeIssue },
issueDetail: { createIssueLink, updateIssueLink, deleteIssueLink }, issueDetail: { createIssueLink, updateIssueLink, deleteIssueLink },
} = useMobxStore(); } = useMobxStore();
const {
currentUser,
membership: { currentProjectRole },
} = useUser();
const { projectStates } = useProjectState();
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId, inboxIssueId } = router.query; const { workspaceSlug, projectId, issueId, inboxIssueId } = router.query;
@ -190,9 +193,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const currentIssueState = projectId const currentIssueState = projectStates?.find((s) => s.id === issueDetail?.state);
? states[projectId.toString()]?.find((s) => s.id === issueDetail?.state)
: undefined;
return ( return (
<> <>
@ -572,7 +573,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
labelList={issueDetail?.labels ?? []} labelList={issueDetail?.labels ?? []}
submitChanges={submitChanges} submitChanges={submitChanges}
isNotAllowed={!isAllowed} isNotAllowed={!isAllowed}
uneditable={uneditable ?? false} uneditable={uneditable || !isAllowed}
/> />
</div> </div>
</div> </div>

View File

@ -78,7 +78,7 @@ export const IssueProperty: React.FC<IIssueProperty> = (props) => {
projectId={issue?.project_detail?.id || null} projectId={issue?.project_detail?.id || null}
value={issue?.state || null} value={issue?.state || null}
onChange={(data) => handleStateChange(data)} onChange={(data) => handleStateChange(data)}
disabled={false} disabled={!editable}
hideDropdownArrow hideDropdownArrow
/> />
</div> </div>
@ -89,7 +89,7 @@ export const IssueProperty: React.FC<IIssueProperty> = (props) => {
value={issue?.assignees || null} value={issue?.assignees || null}
hideDropdownArrow hideDropdownArrow
onChange={(val) => handleAssigneeChange(val)} onChange={(val) => handleAssigneeChange(val)}
disabled={false} disabled={!editable}
/> />
</div> </div>
</div> </div>

View File

@ -3,8 +3,10 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { Plus, ChevronRight, ChevronDown } from "lucide-react"; import { Plus, ChevronRight, ChevronDown } from "lucide-react";
// mobx store // hooks
import { useUser } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast";
// components // components
import { ExistingIssuesListModal } from "components/core"; import { ExistingIssuesListModal } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
@ -12,8 +14,6 @@ import { SubIssuesRootList } from "./issues-list";
import { ProgressBar } from "./progressbar"; import { ProgressBar } from "./progressbar";
// ui // ui
import { CustomMenu } from "@plane/ui"; import { CustomMenu } from "@plane/ui";
// hooks
import useToast from "hooks/use-toast";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
@ -43,13 +43,15 @@ const issueService = new IssueService();
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => { export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
const { parentIssue, user } = props; const { parentIssue, user } = props;
// store hooks
const { const {
user: { currentProjectRole },
issue: { updateIssueStructure }, issue: { updateIssueStructure },
projectIssues: { updateIssue, removeIssue }, projectIssues: { updateIssue, removeIssue },
} = useMobxStore(); } = useMobxStore();
const {
membership: { currentProjectRole },
} = useUser();
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;

View File

@ -1,21 +1,19 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { TwitterPicker } from "react-color"; import { TwitterPicker } from "react-color";
import { Dialog, Popover, Transition } from "@headlessui/react"; import { Dialog, Popover, Transition } from "@headlessui/react";
import { ChevronDown } from "lucide-react";
// store // hooks
import { observer } from "mobx-react-lite"; import { useLabel } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider"; import useToast from "hooks/use-toast";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// icons
import { ChevronDown } from "lucide-react";
// types // types
import type { IIssueLabel, IState } from "types"; import type { IIssueLabel, IState } from "types";
// constants // constants
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label";
import useToast from "hooks/use-toast";
// types // types
type Props = { type Props = {
@ -32,13 +30,14 @@ const defaultValues: Partial<IState> = {
export const CreateLabelModal: React.FC<Props> = observer((props) => { export const CreateLabelModal: React.FC<Props> = observer((props) => {
const { isOpen, projectId, handleClose, onSuccess } = props; const { isOpen, projectId, handleClose, onSuccess } = props;
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store hooks
// store const {
const { projectLabel: projectLabelStore } = useMobxStore(); project: { createLabel },
} = useLabel();
// form info
const { const {
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
handleSubmit, handleSubmit,
@ -72,8 +71,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
const onSubmit = async (formData: IIssueLabel) => { const onSubmit = async (formData: IIssueLabel) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
await projectLabelStore await createLabel(workspaceSlug.toString(), projectId.toString(), formData)
.createLabel(workspaceSlug.toString(), projectId.toString(), formData)
.then((res) => { .then((res) => {
onClose(); onClose();
if (onSuccess) onSuccess(res); if (onSuccess) onSuccess(res);

View File

@ -1,20 +1,18 @@
import React, { forwardRef, useEffect } from "react"; import React, { forwardRef, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { TwitterPicker } from "react-color"; import { TwitterPicker } from "react-color";
import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { Controller, SubmitHandler, useForm } from "react-hook-form";
// stores
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// headless ui
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// hooks
import { useLabel } from "hooks/store";
import useToast from "hooks/use-toast";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// types // types
import { IIssueLabel } from "types"; import { IIssueLabel } from "types";
// fetch-keys // fetch-keys
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label";
import useToast from "hooks/use-toast";
type Props = { type Props = {
labelForm: boolean; labelForm: boolean;
@ -32,16 +30,16 @@ const defaultValues: Partial<IIssueLabel> = {
export const CreateUpdateLabelInline = observer( export const CreateUpdateLabelInline = observer(
forwardRef<HTMLFormElement, Props>(function CreateUpdateLabelInline(props, ref) { forwardRef<HTMLFormElement, Props>(function CreateUpdateLabelInline(props, ref) {
const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props; const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks
// store const {
const { projectLabel: projectLabelStore } = useMobxStore(); project: { createLabel, updateLabel },
} = useLabel();
// toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// form info
const { const {
handleSubmit, handleSubmit,
control, control,
@ -63,8 +61,7 @@ export const CreateUpdateLabelInline = observer(
const handleLabelCreate: SubmitHandler<IIssueLabel> = async (formData) => { const handleLabelCreate: SubmitHandler<IIssueLabel> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting) return;
await projectLabelStore await createLabel(workspaceSlug.toString(), projectId.toString(), formData)
.createLabel(workspaceSlug.toString(), projectId.toString(), formData)
.then(() => { .then(() => {
handleClose(); handleClose();
reset(defaultValues); reset(defaultValues);
@ -82,8 +79,7 @@ export const CreateUpdateLabelInline = observer(
const handleLabelUpdate: SubmitHandler<IIssueLabel> = async (formData) => { const handleLabelUpdate: SubmitHandler<IIssueLabel> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting) return;
await projectLabelStore await updateLabel(workspaceSlug.toString(), projectId.toString(), labelToUpdate?.id!, formData)
.updateLabel(workspaceSlug.toString(), projectId.toString(), labelToUpdate?.id!, formData)
.then(() => { .then(() => {
reset(defaultValues); reset(defaultValues);
handleClose(); handleClose();

View File

@ -1,15 +1,13 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Combobox, Dialog, Transition } from "@headlessui/react";
import { Search } from "lucide-react";
// store // hooks
import { observer } from "mobx-react-lite"; import { useLabel } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider";
// icons // icons
import { LayerStackIcon } from "@plane/ui"; import { LayerStackIcon } from "@plane/ui";
import { Search } from "lucide-react";
// types // types
import { IIssueLabel } from "types"; import { IIssueLabel } from "types";
@ -21,18 +19,15 @@ type Props = {
export const LabelsListModal: React.FC<Props> = observer((props) => { export const LabelsListModal: React.FC<Props> = observer((props) => {
const { isOpen, handleClose, parent } = props; const { isOpen, handleClose, parent } = props;
// states
const [query, setQuery] = useState("");
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks
// store
const { const {
projectLabel: { projectLabels, fetchProjectLabels, updateLabel }, project: { projectLabels, fetchProjectLabels, updateLabel },
} = useMobxStore(); } = useLabel();
// states
const [query, setQuery] = useState("");
// api call to fetch project details // api call to fetch project details
useSWR( useSWR(

View File

@ -1,12 +1,12 @@
import React, { Dispatch, SetStateAction, useState } from "react"; import React, { Dispatch, SetStateAction, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useMobxStore } from "lib/mobx/store-provider";
import { DraggableProvidedDragHandleProps, DraggableStateSnapshot } from "@hello-pangea/dnd"; import { DraggableProvidedDragHandleProps, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { X, Pencil } from "lucide-react";
// hooks
import { useLabel } from "hooks/store";
// types // types
import { IIssueLabel } from "types"; import { IIssueLabel } from "types";
//icons // components
import { X, Pencil } from "lucide-react";
//components
import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
import { CreateUpdateLabelInline } from "./create-update-label-inline"; import { CreateUpdateLabelInline } from "./create-update-label-inline";
@ -21,23 +21,21 @@ type Props = {
export const ProjectSettingLabelItem: React.FC<Props> = (props) => { export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
const { label, setIsUpdating, handleLabelDelete, draggableSnapshot, dragHandleProps, isChild } = props; const { label, setIsUpdating, handleLabelDelete, draggableSnapshot, dragHandleProps, isChild } = props;
const { combineTargetFor, isDragging } = draggableSnapshot; const { combineTargetFor, isDragging } = draggableSnapshot;
// states
const [isEditLabelForm, setEditLabelForm] = useState(false);
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks
// store const {
const { projectLabel: projectLabelStore } = useMobxStore(); project: { updateLabel },
} = useLabel();
//state
const [isEditLabelForm, setEditLabelForm] = useState(false);
const removeFromGroup = (label: IIssueLabel) => { const removeFromGroup = (label: IIssueLabel) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
projectLabelStore.updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, { updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, {
parent: null, parent: null,
}); });
}; };

View File

@ -10,9 +10,8 @@ import {
DropResult, DropResult,
Droppable, Droppable,
} from "@hello-pangea/dnd"; } from "@hello-pangea/dnd";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useLabel } from "hooks/store";
import useDraggableInPortal from "hooks/use-draggable-portal"; import useDraggableInPortal from "hooks/use-draggable-portal";
// components // components
import { import {
@ -32,23 +31,22 @@ import { IIssueLabel } from "types";
const LABELS_ROOT = "labels.root"; const LABELS_ROOT = "labels.root";
export const ProjectSettingsLabelList: React.FC = observer(() => { export const ProjectSettingsLabelList: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const renderDraggable = useDraggableInPortal();
// store
const {
projectLabel: { fetchProjectLabels, projectLabels, updateLabelPosition, projectLabelsTree },
} = useMobxStore();
// states // states
const [showLabelForm, setLabelForm] = useState(false); const [showLabelForm, setLabelForm] = useState(false);
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabel | null>(null); const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabel | null>(null);
const [isDraggingGroup, setIsDraggingGroup] = useState(false); const [isDraggingGroup, setIsDraggingGroup] = useState(false);
// ref // refs
const scrollToRef = useRef<HTMLFormElement>(null); const scrollToRef = useRef<HTMLFormElement>(null);
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { fetchProjectLabels, projectLabels, updateLabelPosition, projectLabelsTree },
} = useLabel();
// portal
const renderDraggable = useDraggableInPortal();
// api call to fetch project details // api call to fetch project details
useSWR( useSWR(

View File

@ -2,12 +2,14 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// hooks // hooks
import { useApplication, useModule } from "hooks/store"; import { useApplication, useModule, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// components // components
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// assets // assets
import emptyModule from "public/empty-state/empty_modules.webp"; import emptyModule from "public/empty-state/empty_modules.webp";
import { NewEmptyState } from "components/common/new-empty-state"; import { NewEmptyState } from "components/common/new-empty-state";
@ -18,10 +20,15 @@ export const ModulesListView: React.FC = observer(() => {
const { workspaceSlug, projectId, peekModule } = router.query; const { workspaceSlug, projectId, peekModule } = router.query;
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { projectModules } = useModule(); const { projectModules } = useModule();
const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); const { storedValue: modulesView } = useLocalStorage("modules_view", "grid");
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (!projectModules) if (!projectModules)
return ( return (
<Loader className="grid grid-cols-3 gap-4 p-8"> <Loader className="grid grid-cols-3 gap-4 p-8">
@ -92,6 +99,7 @@ export const ModulesListView: React.FC = observer(() => {
text: "Build your first module", text: "Build your first module",
onClick: () => commandPaletteStore.toggleCreateModuleModal(true), onClick: () => commandPaletteStore.toggleCreateModuleModal(true),
}} }}
disabled={!isEditingAllowed}
/> />
)} )}
</> </>

View File

@ -13,12 +13,13 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
value: string | null | undefined; value: string | null | undefined;
onChange: (val: string) => void; onChange: (val: string) => void;
disabled?: boolean;
}; };
const projectMemberService = new ProjectMemberService(); const projectMemberService = new ProjectMemberService();
export const SidebarLeadSelect: FC<Props> = (props) => { export const SidebarLeadSelect: FC<Props> = (props) => {
const { value, onChange } = props; const { value, onChange, disabled = false } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -51,6 +52,7 @@ export const SidebarLeadSelect: FC<Props> = (props) => {
</div> </div>
<div className="flex w-1/2 items-center rounded-sm"> <div className="flex w-1/2 items-center rounded-sm">
<CustomSearchSelect <CustomSearchSelect
disabled={disabled}
className="w-full rounded-sm" className="w-full rounded-sm"
value={value} value={value}
customButtonClassName="rounded-sm" customButtonClassName="rounded-sm"
@ -63,7 +65,7 @@ export const SidebarLeadSelect: FC<Props> = (props) => {
) : ( ) : (
<div className="group flex w-full items-center justify-between gap-2 p-1 text-sm text-custom-text-400"> <div className="group flex w-full items-center justify-between gap-2 p-1 text-sm text-custom-text-400">
<span>No lead</span> <span>No lead</span>
<ChevronDown className="hidden h-3.5 w-3.5 group-hover:flex" /> {!disabled && <ChevronDown className="hidden h-3.5 w-3.5 group-hover:flex" />}
</div> </div>
) )
} }

View File

@ -13,12 +13,13 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
value: string[] | undefined; value: string[] | undefined;
onChange: (val: string[]) => void; onChange: (val: string[]) => void;
disabled?: boolean;
}; };
// services // services
const projectMemberService = new ProjectMemberService(); const projectMemberService = new ProjectMemberService();
export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => { export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -48,6 +49,7 @@ export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => {
</div> </div>
<div className="flex w-1/2 items-center rounded-sm "> <div className="flex w-1/2 items-center rounded-sm ">
<CustomSearchSelect <CustomSearchSelect
disabled={disabled}
className="w-full rounded-sm" className="w-full rounded-sm"
value={value ?? []} value={value ?? []}
customButtonClassName="rounded-sm" customButtonClassName="rounded-sm"
@ -67,7 +69,7 @@ export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => {
) : ( ) : (
<div className="group flex w-full items-center justify-between gap-2 p-1 text-sm text-custom-text-400"> <div className="group flex w-full items-center justify-between gap-2 p-1 text-sm text-custom-text-400">
<span>No members</span> <span>No members</span>
<ChevronDown className="hidden h-3.5 w-3.5 group-hover:flex" /> {!disabled && <ChevronDown className="hidden h-3.5 w-3.5 group-hover:flex" />}
</div> </div>
) )
} }

View File

@ -137,7 +137,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
}; };
const handleCopyText = () => { const handleCopyText = () => {
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module?.id}`) copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -238,10 +238,11 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
</Loader> </Loader>
); );
const startDate = new Date(moduleDetails.start_date ?? ""); const startDate = new Date(watch("start_date") ?? moduleDetails.start_date ?? "");
const endDate = new Date(moduleDetails.target_date ?? ""); const endDate = new Date(watch("target_date") ?? moduleDetails.target_date ?? "");
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); const areYearsEqual =
startDate.getFullYear() === endDate.getFullYear() || isNaN(startDate.getFullYear()) || isNaN(endDate.getFullYear());
const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status); const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status);
@ -254,6 +255,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
: `${moduleDetails.total_issues}` : `${moduleDetails.total_issues}`
: `${moduleDetails.completed_issues}/${moduleDetails.total_issues}`; : `${moduleDetails.completed_issues}/${moduleDetails.total_issues}`;
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
return ( return (
<> <>
<LinkModal <LinkModal
@ -283,14 +286,16 @@ export const ModuleDetailsSidebar: 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>
<CustomMenu width="lg" placement="bottom-end" ellipsis> {isEditingAllowed && (
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}> <CustomMenu width="lg" placement="bottom-end" ellipsis>
<span className="flex items-center justify-start gap-2"> <CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
<Trash2 className="h-3 w-3" /> <span className="flex items-center justify-start gap-2">
<span>Delete module</span> <Trash2 className="h-3 w-3" />
</span> <span>Delete module</span>
</CustomMenu.MenuItem> </span>
</CustomMenu> </CustomMenu.MenuItem>
</CustomMenu>
)}
</div> </div>
</div> </div>
@ -304,7 +309,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<CustomSelect <CustomSelect
customButton={ customButton={
<span <span
className="flex h-6 w-20 cursor-default items-center justify-center rounded-sm text-center text-xs" className={`flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
style={{ style={{
color: moduleStatus ? moduleStatus.color : "#a3a3a2", color: moduleStatus ? moduleStatus.color : "#a3a3a2",
backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220", backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220",
@ -317,6 +324,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
onChange={(value: any) => { onChange={(value: any) => {
submitChanges({ status: value }); submitChanges({ status: value });
}} }}
disabled={!isEditingAllowed}
> >
{MODULE_STATUS.map((status) => ( {MODULE_STATUS.map((status) => (
<CustomSelect.Option key={status.value} value={status.value}> <CustomSelect.Option key={status.value} value={status.value}>
@ -332,7 +340,12 @@ export const ModuleDetailsSidebar: 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 className="cursor-default text-sm font-medium text-custom-text-300"> <Popover.Button
className={`text-sm font-medium text-custom-text-300 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
</Popover.Button> </Popover.Button>
@ -353,10 +366,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
handleStartDateChange(val); handleStartDateChange(val);
} }
}} }}
startDate={watch("start_date") ? `${watch("start_date")}` : null} startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ? `${watch("target_date")}` : null} endDate={watch("target_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("target_date")}`)} maxDate={new Date(`${watch("target_date")}`)}
selectsStart selectsStart={watch("target_date") ? true : false}
/> />
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>
@ -364,7 +377,12 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<MoveRight className="h-4 w-4 text-custom-text-300" /> <MoveRight className="h-4 w-4 text-custom-text-300" />
<Popover className="flex h-full items-center justify-center rounded-lg"> <Popover className="flex h-full items-center justify-center rounded-lg">
<> <>
<Popover.Button className="cursor-default text-sm font-medium text-custom-text-300"> <Popover.Button
className={`text-sm font-medium text-custom-text-300 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
</Popover.Button> </Popover.Button>
@ -385,10 +403,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
handleEndDateChange(val); handleEndDateChange(val);
} }
}} }}
startDate={watch("start_date") ? `${watch("start_date")}` : null} startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ? `${watch("target_date")}` : null} endDate={watch("target_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>
@ -410,6 +428,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
name="lead" name="lead"
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<SidebarLeadSelect <SidebarLeadSelect
disabled={!isEditingAllowed}
value={value} value={value}
onChange={(val: string) => { onChange={(val: string) => {
submitChanges({ lead: val }); submitChanges({ lead: val });
@ -422,6 +441,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
name="members" name="members"
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<SidebarMembersSelect <SidebarMembersSelect
disabled={!isEditingAllowed}
value={value} value={value}
onChange={(val: string[]) => { onChange={(val: string[]) => {
submitChanges({ members: val }); submitChanges({ members: val });
@ -546,15 +566,17 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="mt-2 flex h-72 w-full flex-col space-y-3 overflow-y-auto"> <div className="mt-2 flex h-72 w-full flex-col space-y-3 overflow-y-auto">
{currentProjectRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? ( {currentProjectRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? (
<> <>
<div className="flex w-full items-center justify-end"> {isEditingAllowed && (
<button <div className="flex w-full items-center justify-end">
className="flex items-center gap-1.5 text-sm font-medium text-custom-primary-100" <button
onClick={() => setModuleLinkModal(true)} className="flex items-center gap-1.5 text-sm font-medium text-custom-primary-100"
> onClick={() => setModuleLinkModal(true)}
<Plus className="h-3 w-3" /> >
Add link <Plus className="h-3 w-3" />
</button> Add link
</div> </button>
</div>
)}
<LinksList <LinksList
links={moduleDetails.link_module} links={moduleDetails.link_module}

View File

@ -14,7 +14,7 @@ import { IUser } from "types";
// services // services
import { FileService } from "services/file.service"; import { FileService } from "services/file.service";
// assets // assets
import IssuesSvg from "public/onboarding/onboarding-issues.svg"; import IssuesSvg from "public/onboarding/onboarding-issues.webp";
const defaultValues: Partial<IUser> = { const defaultValues: Partial<IUser> = {
first_name: "", first_name: "",

View File

@ -8,6 +8,8 @@ import { useApplication, useProject, useUser } from "hooks/store";
import { TourRoot } from "components/onboarding"; import { TourRoot } from "components/onboarding";
import { UserGreetingsView } from "components/user"; import { UserGreetingsView } from "components/user";
import { CompletedIssuesGraph, IssuesList, IssuesPieChart, IssuesStats } from "components/workspace"; import { CompletedIssuesGraph, IssuesList, IssuesPieChart, IssuesStats } from "components/workspace";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// images // images
import { NewEmptyState } from "components/common/new-empty-state"; import { NewEmptyState } from "components/common/new-empty-state";
import emptyProject from "public/empty-state/dashboard_empty_project.webp"; import emptyProject from "public/empty-state/dashboard_empty_project.webp";
@ -31,6 +33,8 @@ export const WorkspaceDashboardView = observer(() => {
workspaceSlug ? () => fetchUserDashboardInfo(workspaceSlug.toString(), month) : null workspaceSlug ? () => fetchUserDashboardInfo(workspaceSlug.toString(), month) : null
); );
const isEditingAllowed = !!userStore.currentProjectRole && userStore.currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const handleTourCompleted = () => { const handleTourCompleted = () => {
updateTourCompleted() updateTourCompleted()
.then(() => { .then(() => {
@ -90,6 +94,7 @@ export const WorkspaceDashboardView = observer(() => {
commandPaletteStore.toggleCreateProjectModal(true); commandPaletteStore.toggleCreateProjectModal(true);
}, },
}} }}
disabled={!isEditingAllowed}
/> />
) )
) : null} ) : null}

View File

@ -31,18 +31,7 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const canUserCreatePage = const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
const emptyStatePrimaryButton = canUserCreatePage
? {
primaryButton: {
icon: <Plus className="h-4 w-4" />,
text: "Create your first page",
onClick: () => toggleCreatePageModal(true),
},
}
: {};
return ( return (
<> <>
@ -70,7 +59,12 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
"We wrote Parth and Meeras love story. You could write your projects mission, goals, and eventual vision.", "We wrote Parth and Meeras love story. You could write your projects mission, goals, and eventual vision.",
direction: "right", direction: "right",
}} }}
{...emptyStatePrimaryButton} primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "Create your first page",
onClick: () => toggleCreatePageModal(true),
}}
disabled={!isEditingAllowed}
/> />
)} )}
</div> </div>

View File

@ -2,7 +2,7 @@ import React, { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// hooks // hooks
import { useApplication, usePage } from "hooks/store"; import { useApplication, usePage, useUser } from "hooks/store";
// components // components
import { PagesListView } from "components/pages/pages-list"; import { PagesListView } from "components/pages/pages-list";
import { NewEmptyState } from "components/common/new-empty-state"; import { NewEmptyState } from "components/common/new-empty-state";
@ -12,14 +12,21 @@ import { Loader } from "@plane/ui";
import emptyPage from "public/empty-state/empty_page.png"; import emptyPage from "public/empty-state/empty_page.png";
// helpers // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const RecentPagesList: FC = observer(() => { export const RecentPagesList: FC = observer(() => {
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { recentProjectPages } = usePage(); const { recentProjectPages } = usePage();
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value) => value.length === 0); const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value) => value.length === 0);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (!recentProjectPages) { if (!recentProjectPages) {
return ( return (
<Loader className="space-y-4"> <Loader className="space-y-4">
@ -64,6 +71,7 @@ export const RecentPagesList: FC = observer(() => {
text: "Create your first page", text: "Create your first page",
onClick: () => commandPaletteStore.toggleCreatePageModal(true), onClick: () => commandPaletteStore.toggleCreatePageModal(true),
}} }}
disabled={!isEditingAllowed}
/> />
</> </>
)} )}

View File

@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useApplication, useProject } from "hooks/store"; import { useApplication, useProject, useUser } from "hooks/store";
// components // components
import { ProjectCard } from "components/project"; import { ProjectCard } from "components/project";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
@ -8,6 +8,8 @@ import { Loader } from "@plane/ui";
import emptyProject from "public/empty-state/empty_project.webp"; import emptyProject from "public/empty-state/empty_project.webp";
// icons // icons
import { NewEmptyState } from "components/common/new-empty-state"; import { NewEmptyState } from "components/common/new-empty-state";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectCardList = observer(() => { export const ProjectCardList = observer(() => {
// store hooks // store hooks
@ -15,9 +17,14 @@ export const ProjectCardList = observer(() => {
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
eventTracker: { setTrackElement }, eventTracker: { setTrackElement },
} = useApplication(); } = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { workspaceProjects, searchedProjects, getProjectById } = useProject(); const { workspaceProjects, searchedProjects, getProjectById } = useProject();
if (!workspaceProjects) { const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (!workspaceProjects)
return ( return (
<Loader className="grid grid-cols-3 gap-4"> <Loader className="grid grid-cols-3 gap-4">
<Loader.Item height="100px" /> <Loader.Item height="100px" />
@ -28,7 +35,6 @@ export const ProjectCardList = observer(() => {
<Loader.Item height="100px" /> <Loader.Item height="100px" />
</Loader> </Loader>
); );
}
return ( return (
<> <>
@ -65,6 +71,7 @@ export const ProjectCardList = observer(() => {
commandPaletteStore.toggleCreateProjectModal(true); commandPaletteStore.toggleCreateProjectModal(true);
}, },
}} }}
disabled={!isEditingAllowed}
/> />
)} )}
</> </>

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