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
```bash
git clone https://github.com/makeplane/plane
cd plane
git clone https://github.com/makeplane/plane.git [folder-name]
cd [folder-name]
chmod +x setup.sh
```
@ -44,33 +44,12 @@ chmod +x setup.sh
./setup.sh
```
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
3. Start the containers
```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
docker compose -f docker-compose-local.yml up
```
```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
```
4. Run Docker compose up
```bash
docker compose up -d
```
5. Install dependencies
```bash
yarn install
```
6. Run the web app in development mode
```bash
yarn dev
```
## Missing a Feature?

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.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting/docker-compose).
## ⚡️ Contributors Quick Start
@ -63,7 +63,7 @@ Thats it!
## 🍙 Self Hosting
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
## 🚀 Features

View File

@ -49,5 +49,5 @@ USER captain
# Expose container port and run entry point script
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()
)
@ -160,16 +170,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
projects = (
self.get_queryset()
.annotate(sort_order=Subquery(sort_order_query))
.prefetch_related(
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=slug,
is_active=True,
).select_related("member"),
to_attr="members_list",
)
)
.order_by("sort_order", "name")
)
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
@ -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(
bulk_project_members,
batch_size=10,

View File

@ -70,6 +70,7 @@ from plane.app.permissions import (
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
WorkspaceViewerPermission,
WorkspaceUserPermission,
)
from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters
@ -501,6 +502,18 @@ class WorkSpaceMemberViewSet(BaseViewSet):
WorkspaceEntityPermission,
]
def get_permissions(self):
if self.action == "leave":
self.permission_classes = [
WorkspaceUserPermission,
]
else:
self.permission_classes = [
WorkspaceEntityPermission,
]
return super(WorkSpaceMemberViewSet, self).get_permissions()
search_fields = [
"member__display_name",
"member__first_name",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 X-Real-IP $remote_addr;
}
location /space/ {
location /spaces/ {
proxy_pass http://localhost:4000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;

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
cp ./web/.env.example ./web/.env
cp ./space/.env.example ./space/.env
cp ./apiserver/.env.example ./apiserver/.env
# Generate the SECRET_KEY that will be used by django

View File

@ -7,5 +7,8 @@ WORKDIR /app
COPY . .
RUN yarn global add turbo
RUN yarn install
EXPOSE 3000
EXPOSE 4000
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
VOLUME [ "/app/node_modules", "/app/space/node_modules"]
CMD ["yarn","dev", "--filter=space"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,11 +3,10 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { CycleService } from "services/cycle.service";
// hooks
import { useApplication, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { SidebarProgressStats } from "components/core";
@ -30,6 +29,8 @@ import {
} from "helpers/date-time.helper";
// types
import { ICycle } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// fetch-keys
import { CYCLE_STATUS } from "constants/cycle";
@ -44,18 +45,21 @@ const cycleService = new CycleService();
// TODO: refactor the whole component
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { cycleId, handleClose } = props;
// states
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query;
// store hooks
const {
cycle: cycleDetailsStore,
trackEvent: { setTrackElement },
} = useMobxStore();
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { getCycleById, updateCycleDetails } = useCycle();
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
const cycleDetails = getCycleById(cycleId);
const { setToastAlert } = useToast();
@ -71,7 +75,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const submitChanges = (data: Partial<ICycle>) => {
if (!workspaceSlug || !projectId || !cycleId) return;
cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
};
const handleCopyText = () => {
@ -270,10 +274,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</Loader>
);
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const endDate = new Date(watch("end_date") ?? cycleDetails.end_date ?? "");
const startDate = new Date(watch("start_date") ?? cycleDetails.start_date ?? "");
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const areYearsEqual =
startDate.getFullYear() === endDate.getFullYear() || isNaN(startDate.getFullYear()) || isNaN(endDate.getFullYear());
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
@ -286,6 +291,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
: `${cycleDetails.total_issues}`
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
{cycleDetails && workspaceSlug && projectId && (
@ -312,7 +319,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<button onClick={handleCopyText}>
<LinkIcon className="h-3 w-3 text-custom-text-300" />
</button>
{!isCompleted && (
{!isCompleted && isEditingAllowed && (
<CustomMenu width="lg" placement="bottom-end" ellipsis>
<CustomMenu.MenuItem
onClick={() => {
@ -349,8 +356,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="relative flex h-full w-52 items-center gap-2.5">
<Popover className="flex h-full items-center justify-center rounded-lg">
<Popover.Button
disabled={isCompleted ?? false}
className="cursor-default text-sm font-medium text-custom-text-300"
className={`text-sm font-medium text-custom-text-300 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
>
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
</Popover.Button>
@ -373,10 +382,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
handleStartDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("end_date") ? `${watch("end_date")}` : null}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("end_date")}`)}
selectsStart
selectsStart={watch("end_date") ? true : false}
/>
</Popover.Panel>
</Transition>
@ -385,8 +394,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<Popover className="flex h-full items-center justify-center rounded-lg">
<>
<Popover.Button
disabled={isCompleted ?? false}
className="cursor-default text-sm font-medium text-custom-text-300"
className={`text-sm font-medium text-custom-text-300 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
>
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
</Popover.Button>
@ -409,10 +420,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
handleEndDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("end_date") ? `${watch("end_date")}` : null}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd
selectsEnd={watch("start_date") ? true : false}
/>
</Popover.Panel>
</Transition>

View File

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

View File

@ -1,9 +1,9 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useApplication, useLabel, useProject, useProjectState, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// components
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";
export const CycleIssuesHeader: React.FC = observer(() => {
// states
const [analyticsModal, setAnalyticsModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query as {
workspaceSlug: string;
projectId: string;
cycleId: string;
};
// store hooks
const {
cycle: cycleStore,
projectIssuesFilter: projectIssueFiltersStore,
project: { currentProjectDetails },
projectMember: { projectMembers },
projectLabel: { projectLabels },
projectState: projectStateStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
cycleIssuesFilter: { issueFilters, updateFilters },
user: { currentProjectRole },
} = 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;
@ -156,7 +163,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
key={cycle.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)}
>
<div className="flex items-center gap-1.5">
<ContrastIcon className="h-3 w-3" />
{truncateText(cycle.name, 40)}
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@ -177,9 +187,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels ?? undefined}
labels={projectLabels}
members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
states={projectStates}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
@ -193,20 +203,23 @@ export const CycleIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
{canUserCreateIssue && (
<>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
{canUserCreateIssue && (
<Button
onClick={() => {
setTrackElement("CYCLE_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
toggleCreateIssueModal(true, EProjectStore.CYCLE);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)}
<button
type="button"

View File

@ -2,7 +2,8 @@ import { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
// hooks
import { useLabel, useUser } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues";
@ -16,6 +17,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EFilterType } from "store_legacy/issues/types";
import { EUserWorkspaceRoles } from "constants/workspace";
const GLOBAL_VIEW_LAYOUTS = [
{ key: "list", title: "List", link: "/workspace-views", icon: List },
@ -28,19 +30,23 @@ type Props = {
export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const { activeLayout } = props;
// states
const [createViewModal, setCreateViewModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug } = router.query as { workspaceSlug: string };
const { workspaceSlug } = router.query;
// store hooks
const {
workspace: { workspaceLabels },
workspaceMember: { workspaceMembers },
project: { workspaceProjects },
workspaceGlobalIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore();
const {
membership: { currentWorkspaceRole },
} = useUser();
const {
workspace: { workspaceLabels },
} = useLabel();
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
@ -56,7 +62,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
else newValues.push(value);
}
updateFilters(workspaceSlug, EFilterType.FILTERS, { [key]: newValues });
updateFilters(workspaceSlug.toString(), EFilterType.FILTERS, { [key]: newValues });
},
[workspaceSlug, issueFilters, updateFilters]
);
@ -64,7 +70,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug) return;
updateFilters(workspaceSlug, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
updateFilters(workspaceSlug.toString(), EFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
},
[workspaceSlug, updateFilters]
);
@ -72,11 +78,13 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug) return;
updateFilters(workspaceSlug, EFilterType.DISPLAY_PROPERTIES, property);
updateFilters(workspaceSlug.toString(), EFilterType.DISPLAY_PROPERTIES, property);
},
[workspaceSlug, updateFilters]
);
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
@ -142,10 +150,11 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
</FiltersDropdown>
</>
)}
{isAuthorizedUser && (
<Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}>
New View
</Button>
)}
</div>
</div>
</>

View File

@ -1,9 +1,9 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useApplication, useLabel, useProject, useProjectState, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
@ -11,7 +11,7 @@ import { ProjectAnalyticsModal } from "components/analytics";
// ui
import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui";
// icons
import { ArrowRight, ContrastIcon, Plus } from "lucide-react";
import { ArrowRight, Plus } from "lucide-react";
// helpers
import { truncateText } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
@ -25,28 +25,33 @@ import { EFilterType } from "store_legacy/issues/types";
import { EProjectStore } from "store_legacy/command-palette.store";
export const ModuleIssuesHeader: React.FC = observer(() => {
// states
const [analyticsModal, setAnalyticsModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
projectId: string;
moduleId: string;
};
// store hooks
const {
module: moduleStore,
project: projectStore,
projectMember: { projectMembers },
projectState: projectStateStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
projectLabel: { projectLabels },
moduleIssuesFilter: { issueFilters, updateFilters },
user: { currentProjectRole },
} = useMobxStore();
const { currentProjectDetails } = projectStore;
const {
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");
@ -144,7 +149,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<CustomMenu
label={
<>
<ContrastIcon className="h-3 w-3" />
<DiceIcon className="h-3 w-3" />
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
</>
}
@ -157,7 +162,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
key={module.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)}
>
<div className="flex items-center gap-1.5">
<DiceIcon className="h-3 w-3" />
{truncateText(module.name, 40)}
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@ -178,9 +186,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels ?? undefined}
labels={projectLabels}
members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
states={projectStates}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
@ -194,20 +202,23 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
{canUserCreateIssue && (
<>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
{canUserCreateIssue && (
<Button
onClick={() => {
setTrackElement("MODULE_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
toggleCreateIssueModal(true, EProjectStore.MODULE);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)}
<button
type="button"

View File

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

View File

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

View File

@ -202,10 +202,12 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</span>
</Link>
)}
{canUserCreateIssue && (
<>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
{canUserCreateIssue && (
<Button
onClick={() => {
setTrackElement("PROJECT_PAGE_HEADER");
@ -216,6 +218,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
>
Add Issue
</Button>
</>
)}
</div>
</div>

View File

@ -2,7 +2,8 @@ import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// mobx store
// hooks
import { useApplication, useLabel, useProject, useProjectState, useUser } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider";
// components
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";
export const ProjectViewIssuesHeader: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query as {
workspaceSlug: string;
projectId: string;
viewId: string;
};
// store hooks
const {
project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore,
projectViews: projectViewsStore,
viewIssuesFilter: { issueFilters, updateFilters },
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
user: { currentProjectRole },
} = 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;
@ -139,7 +147,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
key={view.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/views/${view.id}`)}
>
<div className="flex items-center gap-1.5">
<PhotoFilterIcon height={12} width={12} />
{truncateText(view.name, 40)}
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@ -153,16 +164,17 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end">
<FiltersDropdown title="Filters" placement="bottom-end" disabled={!canUserCreateIssue}>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels ?? undefined}
labels={projectLabels}
members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
states={projectStates}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
@ -176,18 +188,18 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
{
{canUserCreateIssue && (
<Button
onClick={() => {
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
}
)}
</div>
</div>
);

View File

@ -2,11 +2,13 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks
import { useApplication, useProject } from "hooks/store";
import { useApplication, useProject, useUser } from "hooks/store";
// components
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectViewsHeader: React.FC = observer(() => {
// router
@ -16,8 +18,14 @@ export const ProjectViewsHeader: React.FC = observer(() => {
const {
commandPalette: { toggleCreateViewModal },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const canUserCreateIssue =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
return (
<>
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
@ -52,6 +60,7 @@ export const ProjectViewsHeader: React.FC = observer(() => {
</Breadcrumbs>
</div>
</div>
{canUserCreateIssue && (
<div className="flex flex-shrink-0 items-center gap-2">
<div>
<Button
@ -64,6 +73,7 @@ export const ProjectViewsHeader: React.FC = observer(() => {
</Button>
</div>
</div>
)}
</div>
</>
);

View File

@ -1,9 +1,11 @@
import { observer } from "mobx-react-lite";
import { Search, Plus, Briefcase } from "lucide-react";
// hooks
import { useApplication, useProject } from "hooks/store";
import { useApplication, useProject, useUser } from "hooks/store";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectsHeader = observer(() => {
// store hooks
@ -11,8 +13,13 @@ export const ProjectsHeader = observer(() => {
commandPalette: commandPaletteStore,
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
const { workspaceProjects, searchQuery, setSearchQuery } = useProject();
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
@ -38,7 +45,7 @@ export const ProjectsHeader = observer(() => {
/>
</div>
)}
{isAuthorizedUser && (
<Button
prependIcon={<Plus />}
size="sm"
@ -49,6 +56,7 @@ export const ProjectsHeader = observer(() => {
>
Add Project
</Button>
)}
</div>
</div>
);

View File

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

View File

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

View File

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

View File

@ -135,7 +135,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
debouncedFormSave();
}}
required
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
hasError={Boolean(errors?.description)}
role="textbox"
disabled={!isAllowed}

View File

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

View File

@ -15,10 +15,11 @@ type Props = {
issues: IIssueResponse | undefined;
issueIdList: string[] | null;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
showAllIssues?: boolean;
};
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const { issues, issueIdList, quickActions } = props;
const { issues, issueIdList, quickActions, showAllIssues = false } = props;
// router
const router = useRouter();
@ -52,7 +53,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
return (
<>
{issueIdList?.map((issueId, index) => {
{issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => {
if (!issues?.[issueId]) return null;
const issue = issues?.[issueId];

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
// hooks
import { useApplication } from "hooks/store";
import { useApplication, useUser } from "hooks/store";
// components
import { NewEmptyState } from "components/common/new-empty-state";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// assets
import emptyIssue from "public/empty-state/empty_issues.webp";
import { EProjectStore } from "store_legacy/command-palette.store";
@ -14,6 +16,11 @@ export const ProjectEmptyState: React.FC = observer(() => {
commandPalette: commandPaletteStore,
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return (
<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);
},
}}
disabled={!isEditingAllowed}
/>
</div>
);

View File

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

View File

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

View File

@ -9,10 +9,11 @@ type Props = {
handleRemove: (val: string) => void;
members: IUserLite[] | undefined;
values: string[];
editable: boolean | undefined;
};
export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
const { handleRemove, members, values } = props;
const { handleRemove, members, values, editable } = props;
return (
<>
@ -25,6 +26,7 @@ export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
<div key={memberId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<Avatar name={memberDetails.display_name} src={memberDetails.avatar} showTooltip={false} />
<span className="normal-case">{memberDetails.display_name}</span>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
@ -32,6 +34,7 @@ export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}

View File

@ -9,10 +9,11 @@ import { TIssuePriorities } from "types";
type Props = {
handleRemove: (val: string) => void;
values: string[];
editable: boolean | undefined;
};
export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values } = props;
const { handleRemove, values, editable } = props;
return (
<>
@ -20,6 +21,7 @@ export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
<div key={priority} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<PriorityIcon priority={priority as TIssuePriorities} className={`h-3 w-3`} />
{priority}
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
@ -27,6 +29,7 @@ export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
))}
</>

View File

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

View File

@ -10,10 +10,11 @@ type Props = {
handleRemove: (val: string) => void;
states: IState[];
values: string[];
editable: boolean | undefined;
};
export const AppliedStateFilters: React.FC<Props> = observer((props) => {
const { handleRemove, states, values } = props;
const { handleRemove, states, values, editable } = props;
return (
<>
@ -26,6 +27,7 @@ export const AppliedStateFilters: React.FC<Props> = observer((props) => {
<div key={stateId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<StateGroupIcon color={stateDetails.color} stateGroup={stateDetails.group} height="12px" width="12px" />
{stateDetails.name}
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
@ -33,6 +35,7 @@ export const AppliedStateFilters: React.FC<Props> = observer((props) => {
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}

View File

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

View File

@ -105,14 +105,11 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const displayProperties = issuesFilterStore?.issueFilters?.displayProperties || null;
const sub_group_by: string | null = displayFilters?.sub_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 currentKanBanView: "swimlanes" | "default" = sub_group_by ? "swimlanes" : "default";
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
@ -256,8 +253,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
</Droppable>
</div>
{currentKanBanView === "default" ? (
<KanBan
<KanBanView
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
@ -276,28 +272,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
currentStore={currentStore}
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>
</div>

View File

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

View File

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

View File

@ -100,7 +100,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
const label = (
<Tooltip tooltipHeading="Assignee" tooltipContent={getTooltipContent()} position="top">
<div className="flex h-full w-full cursor-pointer items-center gap-2 text-custom-text-200">
<div className="flex h-full w-full items-center gap-2 text-custom-text-200">
{value && value.length > 0 && Array.isArray(value) ? (
<AvatarGroup showTooltip={false}>
{value.map((assigneeId) => {

View File

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

View File

@ -2,8 +2,9 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// mobx store
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useCycle } from "hooks/store";
// components
import {
CycleAppliedFiltersRoot,
@ -29,12 +30,13 @@ export const CycleLayoutRoot: React.FC = observer(() => {
projectId: string;
cycleId: string;
};
// store hooks
const {
cycle: cycleStore,
cycleIssues: { loader, getIssues, fetchIssues },
cycleIssuesFilter: { issueFilters, fetchFilters },
} = useMobxStore();
const { getCycleById } = useCycle();
useSWR(
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 cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined;
const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
const cycleStatus =
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 = {
issue: IIssue;
members: IUserLite[] | undefined;
onChange: (data: Partial<IIssue>) => void;
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
@ -18,7 +18,7 @@ type Props = {
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
@ -26,8 +26,13 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
projectId={issue.project_detail?.id ?? null}
value={issue.assignees}
defaultOptions={issue?.assignee_details ? issue.assignee_details : []}
onChange={(data) => onChange({ assignees: data })}
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
onChange={(data) => {
onChange(issue, { assignees: data });
if (issue.parent) {
mutateSubIssues(issue, { assignees: data });
}
}}
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
noLabelBorder
hideDropdownArrow

View File

@ -18,7 +18,7 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
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"}
</div>

View File

@ -19,7 +19,7 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
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)}
</div>

View File

@ -9,7 +9,7 @@ import { IIssue } from "types";
type Props = {
issue: IIssue;
onChange: (data: Partial<IIssue>) => void;
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
@ -17,14 +17,19 @@ type Props = {
export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
<ViewDueDateSelect
issue={issue}
onChange={(val) => onChange({ target_date: val })}
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200"
onChange={(val) => {
onChange(issue, { target_date: val });
if (issue.parent) {
mutateSubIssues(issue, { target_date: val });
}
}}
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
noBorder
disabled={disabled}
/>

View File

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

View File

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

View File

@ -18,7 +18,7 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
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"}
</div>

View File

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

View File

@ -9,7 +9,7 @@ import { IIssue } from "types";
type Props = {
issue: IIssue;
onChange: (formData: Partial<IIssue>) => void;
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
@ -17,14 +17,19 @@ type Props = {
export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
<ViewStartDateSelect
issue={issue}
onChange={(val) => onChange({ start_date: val })}
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200"
onChange={(val) => {
onChange(issue, { start_date: val });
if (issue.parent) {
mutateSubIssues(issue, { start_date: val });
}
}}
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
noBorder
disabled={disabled}
/>

View File

@ -6,10 +6,12 @@ import { IssuePropertyState } from "../../properties";
import useSubIssue from "hooks/use-sub-issue";
// types
import { IIssue, IState } from "types";
import { mutate } from "swr";
import { SUB_ISSUES } from "constants/fetch-keys";
type Props = {
issue: IIssue;
onChange: (data: Partial<IIssue>) => void;
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
states: IState[] | undefined;
expandedIssues: string[];
disabled: boolean;
@ -20,7 +22,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
@ -28,7 +30,12 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
projectId={issue.project_detail?.id ?? null}
value={issue.state}
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
onChange={(data) => onChange({ state: data.id, state_detail: data })}
onChange={(data) => {
onChange(issue, { state: data.id, state_detail: data });
if (issue.parent) {
mutateSubIssues(issue, { state: data.id, state_detail: data });
}
}}
className="w-full !h-11 border-b-[0.5px] border-custom-border-200"
buttonClassName="!shadow-none !border-0 h-full w-full"
hideDropdownArrow

View File

@ -18,7 +18,7 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
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"}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,7 +60,9 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, disabl
) : (
<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
</button>

View File

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

View File

@ -4,6 +4,8 @@ import { useRouter } from "next/router";
// components
import { ParentIssuesListModal } from "components/issues";
// icons
import { X } from "lucide-react";
// types
import { IIssue, ISearchIssueResponse } from "types";
@ -32,12 +34,20 @@ export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, d
issueId={issueId as string}
projectId={projectId as string}
/>
<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 "
}`}
onClick={() => setIsParentModalOpen(true)}
onClick={() => {
if (issueDetails?.parent) {
onChange("");
setSelectedParentIssue(null);
} else {
setIsParentModalOpen(true);
}
}}
disabled={disabled}
>
{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>
)}
{issueDetails?.parent && <X className="h-2.5 w-2.5" />}
</button>
</>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,9 +10,8 @@ import {
DropResult,
Droppable,
} from "@hello-pangea/dnd";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useLabel } from "hooks/store";
import useDraggableInPortal from "hooks/use-draggable-portal";
// components
import {
@ -32,23 +31,22 @@ import { IIssueLabel } from "types";
const LABELS_ROOT = "labels.root";
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
const [showLabelForm, setLabelForm] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabel | null>(null);
const [isDraggingGroup, setIsDraggingGroup] = useState(false);
// ref
// refs
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
useSWR(

View File

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

View File

@ -13,12 +13,13 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
value: string | null | undefined;
onChange: (val: string) => void;
disabled?: boolean;
};
const projectMemberService = new ProjectMemberService();
export const SidebarLeadSelect: FC<Props> = (props) => {
const { value, onChange } = props;
const { value, onChange, disabled = false } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -51,6 +52,7 @@ export const SidebarLeadSelect: FC<Props> = (props) => {
</div>
<div className="flex w-1/2 items-center rounded-sm">
<CustomSearchSelect
disabled={disabled}
className="w-full rounded-sm"
value={value}
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">
<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>
)
}

View File

@ -13,12 +13,13 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
value: string[] | undefined;
onChange: (val: string[]) => void;
disabled?: boolean;
};
// services
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 { workspaceSlug, projectId } = router.query;
@ -48,6 +49,7 @@ export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => {
</div>
<div className="flex w-1/2 items-center rounded-sm ">
<CustomSearchSelect
disabled={disabled}
className="w-full rounded-sm"
value={value ?? []}
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">
<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>
)
}

View File

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

View File

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

View File

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

View File

@ -31,18 +31,7 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const canUserCreatePage =
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),
},
}
: {};
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
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.",
direction: "right",
}}
{...emptyStatePrimaryButton}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "Create your first page",
onClick: () => toggleCreatePageModal(true),
}}
disabled={!isEditingAllowed}
/>
)}
</div>

View File

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

View File

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

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