mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'refactor/mobx-store' of github.com:makeplane/plane into refactor/mobx-store
This commit is contained in:
commit
9e9699d2d3
@ -33,8 +33,8 @@ The backend is a django project which is kept inside apiserver
|
||||
1. Clone the repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/makeplane/plane
|
||||
cd plane
|
||||
git clone https://github.com/makeplane/plane.git [folder-name]
|
||||
cd [folder-name]
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
@ -44,33 +44,12 @@ chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||
3. Start the containers
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||
docker compose -f docker-compose-local.yml up
|
||||
```
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
|
||||
```
|
||||
|
||||
4. Run Docker compose up
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. Install dependencies
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
6. Run the web app in development mode
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Missing a Feature?
|
||||
|
||||
|
@ -37,7 +37,7 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
|
||||
|
||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting/docker-compose).
|
||||
|
||||
## ⚡️ Contributors Quick Start
|
||||
|
||||
@ -63,7 +63,7 @@ Thats it!
|
||||
|
||||
## 🍙 Self Hosting
|
||||
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
|
@ -49,5 +49,5 @@ USER captain
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
# CMD [ "./bin/takeoff" ]
|
||||
CMD [ "./bin/takeoff.local" ]
|
||||
|
||||
|
31
apiserver/bin/takeoff.local
Executable file
31
apiserver/bin/takeoff.local
Executable 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
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -65,7 +65,7 @@ def send_export_email(email, slug, csv_buffer, rows):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -51,7 +51,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -41,7 +41,7 @@ def magic_link(email, key, token, current_site):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -60,7 +60,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -70,7 +70,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -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
10
nginx/Dockerfile.dev
Normal 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"]
|
@ -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
36
nginx/nginx.conf.dev
Normal 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/;
|
||||
}
|
||||
}
|
||||
}
|
1
setup.sh
1
setup.sh
@ -6,7 +6,6 @@ export LC_ALL=C
|
||||
export LC_CTYPE=C
|
||||
|
||||
cp ./web/.env.example ./web/.env
|
||||
cp ./space/.env.example ./space/.env
|
||||
cp ./apiserver/.env.example ./apiserver/.env
|
||||
|
||||
# Generate the SECRET_KEY that will be used by django
|
||||
|
@ -7,5 +7,8 @@ WORKDIR /app
|
||||
COPY . .
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
EXPOSE 3000
|
||||
EXPOSE 4000
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/space/node_modules"]
|
||||
CMD ["yarn","dev", "--filter=space"]
|
||||
|
@ -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"]
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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" />
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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} />}
|
||||
|
@ -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({
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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"
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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];
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
@ -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>
|
||||
|
@ -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) => {
|
||||
|
@ -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) => ({
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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!,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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(
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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: "",
|
||||
|
@ -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}
|
||||
|
@ -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 Meera’s love story. You could write your project’s 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>
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user